minimal-agent 0.9.0 → 0.10.0

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 (467) hide show
  1. package/README.md +11 -8
  2. package/package.json +34 -1
  3. package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
  4. package/plugins/cron-runner/commands/cron.md +26 -0
  5. package/plugins/cron-runner/commands/crons.md +7 -0
  6. package/plugins/cron-runner/plugin.js +34 -0
  7. package/plugins/cron-runner/src/commands.js +348 -0
  8. package/plugins/cron-runner/src/cronTime.js +148 -0
  9. package/plugins/cron-runner/src/lock.js +68 -0
  10. package/plugins/cron-runner/src/runs.js +89 -0
  11. package/plugins/cron-runner/src/scheduleEngine.js +72 -0
  12. package/plugins/cron-runner/src/scheduler.js +355 -0
  13. package/plugins/cron-runner/src/schema.js +140 -0
  14. package/plugins/cron-runner/src/service.js +167 -0
  15. package/plugins/cron-runner/src/store.js +298 -0
  16. package/plugins/cron-runner/src/types.js +1 -0
  17. package/plugins/workflow-runner/commands/workflow.md +4 -4
  18. package/plugins/workflow-runner/commands/workflows.md +1 -1
  19. package/plugins/workflow-runner/plugin.js +2 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +172 -9
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +150 -13
  23. package/plugins/workflow-runner/src/index.js +39 -8
  24. package/plugins/workflow-runner/src/loader.js +216 -64
  25. package/plugins/workflow-runner/src/multimodal.js +37 -0
  26. package/plugins/workflow-runner/src/recovery.js +26 -24
  27. package/plugins/workflow-runner/src/runner.js +693 -287
  28. package/plugins/workflow-runner/src/stepExecutors/agent.js +126 -37
  29. package/plugins/workflow-runner/src/stepExecutors/assert.js +3 -2
  30. package/plugins/workflow-runner/src/stepExecutors/llm.js +38 -30
  31. package/plugins/workflow-runner/src/stepExecutors/skill.js +27 -38
  32. package/plugins/workflow-runner/src/stepExecutors/tool.js +56 -3
  33. package/plugins/workflow-runner/src/stepExecutors/verify.js +19 -10
  34. package/plugins/workflow-runner/src/taskIdentity.js +185 -30
  35. package/plugins/workflow-runner/src/types.js +18 -7
  36. package/plugins/workflow-runner/src/workflowState.js +355 -55
  37. package/skills/ai-image/SKILL.md +89 -0
  38. package/skills/ai-image/scripts/generate.mjs +497 -0
  39. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  40. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  41. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  42. package/skills/browser-automation/SKILL.md +47 -0
  43. package/skills/docx/SKILL.md +25 -3
  44. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  45. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  46. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/opc_check.py +253 -0
  53. package/skills/image-gen-openrouter/SKILL.md +10 -2
  54. package/skills/image-stats/SKILL.md +65 -0
  55. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  56. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  57. package/skills/image-stats/scripts/image_stats.py +368 -0
  58. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  59. package/skills/pptx/SKILL.md +123 -95
  60. package/skills/pptx/design.md +82 -0
  61. package/skills/pptx/examples/README.md +29 -0
  62. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  63. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  64. package/skills/pptx/pptxgenjs.md +41 -0
  65. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  66. package/skills/pptx/scripts/layout_lint.py +293 -0
  67. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  68. package/skills/pptx/scripts/opc_check.py +339 -0
  69. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  70. package/skills/pptx/svg.md +67 -0
  71. package/skills/tts-zh/SKILL.md +19 -2
  72. package/skills/ui-design/SKILL.md +158 -0
  73. package/skills/ui-design/claude_design.md +309 -0
  74. package/skills/ui-design/evals/evals.json +254 -0
  75. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  76. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  77. package/skills/ui-design/references/design-context.md +53 -0
  78. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  79. package/skills/ui-design/references/export-pptx.md +124 -0
  80. package/skills/ui-design/references/layout-composition.md +654 -0
  81. package/skills/ui-design/references/preview-feedback.md +37 -0
  82. package/skills/ui-design/references/source-image-layout.md +189 -0
  83. package/skills/ui-design/references/starter-components.md +78 -0
  84. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  85. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  86. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  87. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  88. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  89. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  90. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  91. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  92. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  93. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  94. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  95. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  96. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  97. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  98. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  99. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  100. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/assemble_material.py +217 -0
  140. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  141. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  142. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  143. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  144. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  145. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  146. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  147. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  148. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  149. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  150. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  151. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  152. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  153. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  154. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  155. package/skills/ui-design/scripts/design_style_check.py +397 -0
  156. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  157. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  158. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  159. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  160. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  161. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  162. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  163. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  164. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  165. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  166. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  167. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  168. package/skills/ui-design/scripts/layout_balance.py +881 -0
  169. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  170. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  171. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  172. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  173. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  174. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  175. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  176. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  177. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  178. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  179. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  180. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  181. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  182. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  183. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  184. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  185. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  186. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  187. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  188. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  195. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  196. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  205. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  206. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  221. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  222. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  225. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  226. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  239. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  240. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  248. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  249. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  261. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  262. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  275. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  292. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  293. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  316. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  324. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  325. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  328. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  329. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  337. package/skills/video-compose/SKILL.md +8 -1
  338. package/skills/web-crawler/SKILL.md +70 -0
  339. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  340. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  341. package/skills/xlsx/SKILL.md +12 -0
  342. package/skills/xlsx/scripts/opc_check.py +253 -0
  343. package/src/advisorState.js +22 -0
  344. package/src/askBridge.js +87 -3
  345. package/src/bootstrap/workingDir.js +12 -1
  346. package/src/cli/args.js +17 -1
  347. package/src/cli/configMenu.js +236 -0
  348. package/src/cli/configWizard.js +7 -5
  349. package/src/cli/print.js +37 -4
  350. package/src/cli/streamJson.js +77 -24
  351. package/src/cli/wizards/advisorWizard.js +147 -0
  352. package/src/cli/wizards/fieldForm.js +149 -0
  353. package/src/cli/wizards/imageWizard.js +60 -0
  354. package/src/cli/wizards/openRouterWizard.js +23 -0
  355. package/src/cli/wizards/providerPresets.js +95 -0
  356. package/src/cli/wizards/providerWizard.js +219 -0
  357. package/src/cli/wizards/searchWizard.js +23 -0
  358. package/src/cli/wizards/ttsWizard.js +34 -0
  359. package/src/config/configFile.js +441 -83
  360. package/src/config/configService.js +446 -0
  361. package/src/config.js +24 -64
  362. package/src/context/agentHome.js +9 -0
  363. package/src/context/archive.js +3 -2
  364. package/src/context/compact.js +8 -5
  365. package/src/context/contextLock.js +81 -0
  366. package/src/context/persistContext.js +30 -11
  367. package/src/context/reactiveCompact.js +4 -2
  368. package/src/context/recentDirs.js +21 -61
  369. package/src/context/sessionPath.js +97 -23
  370. package/src/context/sessionRegistry.js +303 -24
  371. package/src/context/tokenCounter.js +18 -0
  372. package/src/context/workspaceIndex.js +731 -0
  373. package/src/infra/verification.js +3 -2
  374. package/src/llm/client.js +293 -206
  375. package/src/llm/providers/anthropicFormat.js +241 -0
  376. package/src/llm/providers/openaiFormat.js +190 -0
  377. package/src/loop.js +211 -27
  378. package/src/main.js +61 -11
  379. package/src/messageQueue.js +97 -0
  380. package/src/multimodal.js +203 -0
  381. package/src/plugin-sdk.js +15 -0
  382. package/src/plugins/commandRouter.js +3 -0
  383. package/src/plugins/hookEngine.js +5 -1
  384. package/src/plugins/pluginLifecycle.js +28 -0
  385. package/src/plugins/pluginRunner.js +40 -0
  386. package/src/plugins/transcript.js +4 -3
  387. package/src/prompts/pluginList.js +2 -2
  388. package/src/prompts/projectInstructions.js +3 -3
  389. package/src/prompts/promptAssembly.js +133 -109
  390. package/src/prompts/skillList.js +12 -22
  391. package/src/session/runAgentTurn.js +70 -0
  392. package/src/session/runTurn.js +41 -6
  393. package/src/toolGate.js +17 -0
  394. package/src/tools/advisor/advisor.js +379 -0
  395. package/src/tools/agent/agent.js +555 -0
  396. package/src/tools/agent/agentTypes.js +59 -0
  397. package/src/tools/agent/coordinator.js +93 -0
  398. package/src/tools/agent/worktree.js +270 -0
  399. package/src/tools/ask/ask.js +7 -2
  400. package/src/tools/bash/bash.js +112 -18
  401. package/src/tools/download/download.js +144 -0
  402. package/src/tools/edit/edit.js +1 -0
  403. package/src/tools/edit/multi-edit.js +1 -0
  404. package/src/tools/genimage/genimage.js +242 -0
  405. package/src/tools/genspeech/genspeech.js +67 -0
  406. package/src/tools/glob/glob.js +1 -0
  407. package/src/tools/grep/grep.js +3 -0
  408. package/src/tools/grep/rgPath.js +2 -0
  409. package/src/tools/index.js +25 -2
  410. package/src/tools/read/read.js +1 -0
  411. package/src/tools/shared/killTree.js +52 -0
  412. package/src/tools/toolProgressBridge.js +48 -0
  413. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  414. package/src/tools/webbrowser/browser.js +59 -42
  415. package/src/tools/webbrowser/browserControl.js +118 -0
  416. package/src/tools/webbrowser/browserDaemon.js +97 -0
  417. package/src/tools/webbrowser/browserService.js +27 -0
  418. package/src/tools/webbrowser/browserStream.js +119 -0
  419. package/src/tools/webbrowser/webbrowser.js +699 -130
  420. package/src/tools/webfetch/webfetch.js +1 -0
  421. package/src/tools/websearch/websearch.js +134 -51
  422. package/src/tools/write/write.js +1 -0
  423. package/src/ui/App.js +249 -22
  424. package/src/ui/AskQuestion.js +19 -17
  425. package/src/ui/InputBox.js +34 -9
  426. package/src/ui/LiveArea.js +112 -19
  427. package/src/ui/MessageList.js +100 -14
  428. package/src/ui/ModelPicker.js +56 -0
  429. package/src/ui/QueuedMessagesBanner.js +14 -0
  430. package/src/ui/Root.js +18 -10
  431. package/src/ui/SessionPicker.js +102 -0
  432. package/src/ui/StatusLine.js +13 -4
  433. package/src/ui/WorkspacePicker.js +55 -0
  434. package/src/ui/figures.js +44 -0
  435. package/src/ui/hooks/askSlot.js +82 -0
  436. package/src/ui/hooks/useChat.js +345 -64
  437. package/src/ui/hooks/usePasteHandler.js +11 -1
  438. package/src/ui/hooks/useTextBuffer.js +1 -3
  439. package/src/ui/liveViewport.js +5 -1
  440. package/src/ui/markdown.js +206 -0
  441. package/src/ui/textBuffer.js +0 -7
  442. package/src/ui/theme.js +32 -0
  443. package/src/utils/debugLog.js +0 -4
  444. package/src/utils/resourcePaths.js +2 -2
  445. package/src/utils/stripThink.js +50 -0
  446. package/src/web/browserBridge.js +447 -0
  447. package/src/web/protocol.js +19 -0
  448. package/workflows/adaptive-research.yaml +9 -2
  449. package/workflows/book-review-short.yaml +31 -7
  450. package/workflows/e2e-write-greet.yaml +3 -0
  451. package/workflows/illustrate.yaml +227 -0
  452. package/workflows/plan.yaml +437 -207
  453. package/workflows/ppt.yaml +1659 -0
  454. package/workflows/ralph-loop.yaml +142 -112
  455. package/workflows/schema.json +81 -23
  456. package/workflows/webppt.yaml +496 -0
  457. package/workflows/word.yaml +710 -0
  458. package/workflows/youtube-shorts.yaml +38 -10
  459. package/MINIMAL-TRACE.md +0 -77
  460. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  461. package/skills/config/SKILL.md +0 -123
  462. package/skills/frontend-design/LICENSE.txt +0 -177
  463. package/skills/frontend-design/SKILL.md +0 -42
  464. package/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
  465. package/src/ui/ProgressPanel.js +0 -54
  466. package/src/ui/StreamingBlock.js +0 -11
  467. package/src/ui/ToolStatus.js +0 -11
@@ -13,27 +13,29 @@
13
13
  * - tool / llm / assert —— 原子动作(execXxxStep 返回 StepResult)
14
14
  * - agent / skill —— 有界 TAOR:runQuery(agent) / runQuery(skill);
15
15
  * 透传子事件(stage_change/text/reasoning/tool_*)
16
- * - branch / loop / pause —— 控制流,直接在 runSteps 内处理(不走 executor)
17
- * - parallel / vote —— 并发 / 投票门,也在 runSteps 内处理
16
+ * - branch / loop / pause / echo —— 控制流,直接在 runSteps 内处理(不走 executor)
17
+ * - break / continue —— 循环信号(可带 target 穿透到指定外层循环)
18
18
  *
19
19
  * 控制流嵌套通过 runSteps 递归实现:branch.then/else / loop.do 子数组
20
20
  * 是 StepDef[],调用方语义与顶层 def.steps 一致;变量栈用 VarStack.push/pop
21
21
  * 在 loop 迭代间隔离 `${as}` / `${as}_idx`。
22
22
  * ============================================================
23
23
  */
24
- import { getWorkingDir } from '../../../src/plugin-sdk.js';
25
- import { dlog } from '../../../src/utils/debugLog.js';
26
- import { evalExpr, interpolate } from './expressions.js';
24
+ import { existsSync } from 'node:fs';
25
+ import { resolve } from 'node:path';
26
+ import { dlog, executeTool, getWorkingDir } from '../../../src/plugin-sdk.js';
27
+ import { isMaxTurnsCutoff, truncate } from './agentCommon.js';
28
+ import { evalExpr, interpolate, interpolateDeep, interpolateTyped } from './expressions.js';
27
29
  import { runRecovery } from './recovery.js';
30
+ import { buildTaskArbitrator, isContinuationPhrase } from './taskIdentity.js';
28
31
  import { execAgentStep } from './stepExecutors/agent.js';
29
32
  import { execAssertStep } from './stepExecutors/assert.js';
30
33
  import { execVerifyStep } from './stepExecutors/verify.js';
31
34
  import { execLlmStep } from './stepExecutors/llm.js';
32
35
  import { execSkillStep } from './stepExecutors/skill.js';
33
36
  import { execToolStep } from './stepExecutors/tool.js';
34
- import { VarStack } from './types.js';
35
- import { runVoter } from './voteHelpers.js';
36
- import { WorkflowState } from './workflowState.js';
37
+ import { normalizeCapture, VarStack } from './types.js';
38
+ import { loopCheckpointName, WorkflowState, } from './workflowState.js';
37
39
  /**
38
40
  * 插件 ID:把私有 workflow_* 事件镜像成框架通用 plugin_progress 时挂这个 id。
39
41
  * LiveArea 状态行会渲染成 "[plugin:workflow-runner] i/total <message>"
@@ -75,11 +77,40 @@ function buildGoalAnchor(inputDefs, inputValues) {
75
77
  }
76
78
  if (!goalValue)
77
79
  return '';
78
- // 截断过长目标(>200 字符时保留首尾,中间用 … 连接)
79
- const display = goalValue.length > 200
80
- ? `${goalValue.slice(0, 120)}…${goalValue.slice(-60)}`
81
- : goalValue;
82
- return `> 🎯 用户原始目标:${display}\n> ⚠️ 所有操作必须直接回应此目标,不可偏离或遗忘。`;
80
+ return `> 🎯 用户原始目标:${truncateGoal(goalValue)}\n> ⚠️ 所有操作必须直接回应此目标,不可偏离或遗忘。`;
81
+ }
82
+ /** 截断过长目标(>200 字符时保留首尾,中间用 … 连接)——goalAnchor 注入每步 prompt,必须紧凑。 */
83
+ function truncateGoal(goal) {
84
+ return goal.length > 200 ? `${goal.slice(0, 120)}…${goal.slice(-60)}` : goal;
85
+ }
86
+ /** 可中断的退避等待:用户 abort 时立即返回(让后续步骤检查 signal 收口,不在这里抛)。 */
87
+ function sleepInterruptible(ms, signal) {
88
+ return new Promise((resolve) => {
89
+ if (signal?.aborted) {
90
+ resolve();
91
+ return;
92
+ }
93
+ const timer = setTimeout(() => {
94
+ signal?.removeEventListener('abort', onAbort);
95
+ resolve();
96
+ }, ms);
97
+ const onAbort = () => {
98
+ clearTimeout(timer);
99
+ resolve();
100
+ };
101
+ signal?.addEventListener('abort', onAbort, { once: true });
102
+ });
103
+ }
104
+ /** 把一段用户输入并入贯穿目标(内存 + 状态目录持久化,resume 不丢)。 */
105
+ async function addGoalSupplement(ctx, state, text) {
106
+ const t = text.trim();
107
+ if (!t)
108
+ return;
109
+ // 锚定必须紧凑(注入每步 prompt),逐条截断;完整原文在 capture 变量 / history 里
110
+ const compact = t.length > 240 ? `${t.slice(0, 240)}…` : t;
111
+ ctx.goalSupplements ??= [];
112
+ ctx.goalSupplements.push(compact);
113
+ await state.appendSupplement(compact);
83
114
  }
84
115
  function stepKind(step) {
85
116
  if (step.type === 'break')
@@ -107,16 +138,37 @@ function readPath(root, path) {
107
138
  return cur;
108
139
  }
109
140
  function bindCapture(captureMap, result, vars) {
110
- for (const [resultField, varName] of Object.entries(captureMap)) {
141
+ for (const [resultField, varName] of Object.entries(normalizeCapture(captureMap))) {
111
142
  const value = resultField.includes('.')
112
143
  ? readPath(result.raw, resultField)
113
144
  : result.raw[resultField];
114
145
  vars.set(varName, value);
115
146
  }
116
147
  }
148
+ /** 取本步 capture 出的 { varName: value },供 checkpoint 记录(resume 回放用)。 */
149
+ function collectCaptured(captureMap, vars) {
150
+ const out = {};
151
+ for (const varName of Object.values(normalizeCapture(captureMap))) {
152
+ out[varName] = vars.get(varName);
153
+ }
154
+ return out;
155
+ }
156
+ function resolvePositiveIterationLimit(raw, vars, defaultValue, label) {
157
+ if (raw === undefined)
158
+ return { ok: true, value: defaultValue };
159
+ const interpolated = typeof raw === 'number' ? raw : interpolate(String(raw), vars);
160
+ const n = typeof raw === 'number' ? raw : Number(String(interpolated).trim());
161
+ if (!Number.isFinite(n) || n <= 0) {
162
+ return {
163
+ ok: false,
164
+ error: `${label} max_iterations parse failed: ${JSON.stringify(raw)} -> "${String(interpolated)}" is not a positive number`,
165
+ };
166
+ }
167
+ return { ok: true, value: Math.floor(n) };
168
+ }
117
169
  /**
118
170
  * 分发"动作"型 step(tool / agent / llm / skill / assert)到对应 executor。
119
- * 控制流 step(branch / loop / pause / parallel / vote)由 runSteps 直接处理,不走这里。
171
+ * 控制流 step(branch / loop / pause 等)由 runSteps 直接处理,不走这里。
120
172
  */
121
173
  async function* dispatchActionStep(step, vars, ctx) {
122
174
  if (step.type === 'assert')
@@ -134,17 +186,34 @@ async function* dispatchActionStep(step, vars, ctx) {
134
186
  throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert/verify)`);
135
187
  }
136
188
  /**
137
- * 递归执行 steps[]。返回 true 表示该 block 已 halt(上游应中止)。
189
+ * 递归执行 steps[]。返回 StepFlow(completed / halted / break|continue 信号,见上方定义)。
138
190
  *
139
191
  * yields 混合事件:WorkflowEvent + LoopEvent(skill 子循环转发 / error / interrupted)。
140
192
  * index/total 字段相对**当前 block**,嵌套 block 自有计数(branch.then 内 3 步就报 total=3)。
141
193
  */
142
- async function* runSteps(steps, vars, ctx, state) {
194
+ async function* runSteps(steps, vars, ctx, state,
195
+ // checkpoint 记录范围:顶层调用(runWorkflow → def.steps)传 { name: id=>id, descendIntoLoop:true };
196
+ // 顶层 loop 的 do[] 由 loop 处理器派生复合名 scope;更深嵌套传 undefined(不记录)。
197
+ cp,
198
+ // resume:本列表中索引 < startFrom 的步已在上次运行完成,本次跳过(变量已由 runWorkflow 回放)。
199
+ // 顶层与「loop resume 进迭代中间」(loop 处理器对 do[] 传 innerStartFrom)都用它。
200
+ startFrom = 0,
201
+ // 仅顶层传:进行中循环的恢复信息(迭代号 + 迭代内起点),由匹配 id 的 loop 消费一次。
202
+ loopResume) {
143
203
  for (let i = 0; i < steps.length; i++) {
144
204
  const step = steps[i];
145
205
  if (ctx.signal?.aborted) {
146
206
  yield { type: 'interrupted' };
147
- return true;
207
+ return 'halted';
208
+ }
209
+ // resume:跳过已完成的步(变量已回放,无需重跑)。
210
+ if (i < startFrom) {
211
+ yield {
212
+ type: 'workflow_step_skipped',
213
+ id: step.id,
214
+ reason: 'resume: 上次已完成',
215
+ };
216
+ continue;
148
217
  }
149
218
  // when 条件
150
219
  if (step.when) {
@@ -164,7 +233,7 @@ async function* runSteps(steps, vars, ctx, state) {
164
233
  type: 'error',
165
234
  error: `Workflow halted at step "${step.id}": ${errMsg}`,
166
235
  };
167
- return true;
236
+ return 'halted';
168
237
  }
169
238
  if (!pass) {
170
239
  yield {
@@ -178,23 +247,39 @@ async function* runSteps(steps, vars, ctx, state) {
178
247
  }
179
248
  const kind = stepKind(step);
180
249
  dlog('wf', `step ${i + 1}/${steps.length} START ${step.id} (${kind})`);
250
+ // 已插值的人话进度名随 start 事件带给 UI:scrollback 进度行显示它而非块内相对
251
+ // 序号,与状态行(下方 plugin_progress)保持一致。未声明 title 的步省略此字段。
252
+ const interpolatedTitle = step.title
253
+ ? interpolate(step.title, vars)
254
+ : undefined;
181
255
  yield {
182
256
  type: 'workflow_step_start',
183
257
  id: step.id,
184
258
  kind,
185
259
  index: i,
186
260
  total: steps.length,
261
+ title: interpolatedTitle,
187
262
  };
188
- // P1 TUI 可见:把私有 workflow_step_start 镜像成框架通用 plugin_progress,
189
- // LiveArea 状态行渲染成 "[plugin:workflow-runner] i/total step <id> (<kind>)"。
190
- // (嵌套 block 的 i/total 相对当前 block,足够给"跑到哪一步"的实时反馈。)
191
- yield {
192
- type: 'plugin_progress',
193
- pluginId: WF_PLUGIN_ID,
194
- current: i + 1,
195
- max: steps.length,
196
- message: `step ${step.id} (${kind})`,
197
- };
263
+ // P1 TUI 可见:把私有 workflow_step_start 镜像成框架通用 plugin_progress。
264
+ // 有 title 的步显示人话阶段名(如「阶段2/6·起草实现计划」)且不带块内序号——
265
+ // 嵌套块的 i/total(顶层 5、循环体 12…)会被用户误读成"跳步/结构乱";
266
+ // 未声明 title 的步保持 legacy 格式 "i/total step <id> (<kind>)"。
267
+ if (interpolatedTitle) {
268
+ yield {
269
+ type: 'plugin_progress',
270
+ pluginId: WF_PLUGIN_ID,
271
+ message: interpolatedTitle,
272
+ };
273
+ }
274
+ else {
275
+ yield {
276
+ type: 'plugin_progress',
277
+ pluginId: WF_PLUGIN_ID,
278
+ current: i + 1,
279
+ max: steps.length,
280
+ message: `step ${step.id} (${kind})`,
281
+ };
282
+ }
198
283
  // ---------- 控制流:branch ----------
199
284
  if (step.type === 'branch') {
200
285
  let cond = false;
@@ -206,13 +291,14 @@ async function* runSteps(steps, vars, ctx, state) {
206
291
  await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
207
292
  yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
208
293
  yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
209
- return true;
294
+ return 'halted';
210
295
  }
211
296
  const branch = cond ? step.then : step.else;
212
297
  if (Array.isArray(branch) && branch.length > 0) {
213
- const childHalted = yield* runSteps(branch, vars, ctx, state);
214
- if (childHalted)
215
- return true;
298
+ // branch 对 break 透明:子块 break/halt 都原样上抛给最近的循环 / 顶层。
299
+ const r = yield* runSteps(branch, vars, ctx, state);
300
+ if (r !== 'completed')
301
+ return r;
216
302
  }
217
303
  await state.appendProgress(`✓ ${step.id} (branch=${cond})`);
218
304
  await state.writeVars(vars.snapshot());
@@ -224,25 +310,104 @@ async function* runSteps(steps, vars, ctx, state) {
224
310
  };
225
311
  continue;
226
312
  }
313
+ // ---------- 控制流:set(确定性变量赋值) ----------
314
+ //
315
+ // 状态机类工作流(如 ralph-loop 的 phase)必须用 set——曾有 yaml 用
316
+ // `llm: "PLAN"` 当赋值语句:真把 "PLAN" 发给模型、capture 模型自由回复,
317
+ // 变量值非确定,下游 phase == 'PLAN' 比较全部不可靠。
318
+ if (step.type === 'set') {
319
+ const missing = [];
320
+ // 顶层值若是单一 "${EXPR}" 模板走 interpolateTyped 保留类型(布尔/数字/数组
321
+ // 不再字符串化成 "true",下游条件不必写 == true || == 'true' 防御);
322
+ // 多片段模板与嵌套结构行为不变(interpolateDeep)。
323
+ const resolved = {};
324
+ for (const [k, v] of Object.entries(step.set ?? {})) {
325
+ resolved[k] =
326
+ typeof v === 'string'
327
+ ? interpolateTyped(v, vars, { onMissing: (e) => missing.push(e) })
328
+ : interpolateDeep(v, vars, { onMissing: (e) => missing.push(e) });
329
+ }
330
+ if (missing.length > 0) {
331
+ yield {
332
+ type: 'warning',
333
+ message: `step "${step.id}": 模板变量未定义:${[...new Set(missing)].map((m) => `\${${m}}`).join('、')}(裸名请用 inputs.<name> 或先 capture/set)`,
334
+ };
335
+ }
336
+ for (const [k, v] of Object.entries(resolved)) {
337
+ vars.setGlobal(k, v);
338
+ }
339
+ const summary = Object.entries(resolved)
340
+ .map(([k, v]) => `${k}=${String(v).slice(0, 40)}`)
341
+ .join(', ');
342
+ await state.appendProgress(`✓ ${step.id} (set ${summary})`);
343
+ await state.writeVars(vars.snapshot());
344
+ // set 记 checkpoint(值入 captures):resume 跳过它时变量仍能回放
345
+ if (cp) {
346
+ await state.recordCheckpoint(cp.name(step.id), `set: ${summary}`, resolved, vars.snapshot());
347
+ }
348
+ yield {
349
+ type: 'workflow_step_end',
350
+ id: step.id,
351
+ ok: true,
352
+ output: `set: ${summary}`,
353
+ };
354
+ continue;
355
+ }
227
356
  // ---------- 控制流:loop(for-each 或 while) ----------
228
357
  if (step.type === 'loop') {
229
358
  const isWhileMode = step.over === undefined;
230
- const maxIter = step.max_iterations ?? 200; // while 模式安全上限
359
+ // max_iterations 支持 ${} 模板(如 "${inputs.max_iterations}"):插值 + coerce;
360
+ // 失败响亮 halt——曾有模板字符串不插值直接参与 < 比较 → NaN → 静默 0 次迭代。
361
+ let maxIter = 200; // while 模式安全上限(缺省)
362
+ if (step.max_iterations !== undefined) {
363
+ const raw = step.max_iterations;
364
+ const n = typeof raw === 'number'
365
+ ? raw
366
+ : Number(interpolate(String(raw), vars).trim());
367
+ if (!Number.isFinite(n) || n <= 0) {
368
+ const errMsg = `loop "${step.id}" 的 max_iterations 解析失败:${JSON.stringify(raw)} → "${typeof raw === 'string' ? interpolate(String(raw), vars) : raw}" 不是正数(检查变量是否存在,裸名请用 inputs.<name>)`;
369
+ yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
370
+ yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
371
+ return 'halted';
372
+ }
373
+ maxIter = Math.floor(n);
374
+ }
231
375
  const useFreshContext = step.fresh_context === true;
232
376
  // fresh context:循环开始时 snapshot 当前 history
233
377
  let baseHistorySnapshot = null;
234
378
  if (useFreshContext) {
235
379
  baseHistorySnapshot = ctx.history.slice();
236
380
  }
381
+ // loop resume:恢复信息匹配本循环时消费一次——从中断时的迭代号起跑,
382
+ // 该迭代内已完成的步经 innerStartFrom 跳过(变量已由 runWorkflow 回放快照)。
383
+ let resumeIterStart = 0;
384
+ let pendingInnerStart = 0;
385
+ if (loopResume && loopResume.loopId === step.id) {
386
+ resumeIterStart = loopResume.iteration;
387
+ pendingInnerStart = loopResume.innerStartFrom;
388
+ loopResume = undefined;
389
+ yield {
390
+ type: 'warning',
391
+ message: `♻ 循环 ${step.id} 从第 ${resumeIterStart + 1} 轮迭代第 ${pendingInnerStart + 1} 步续跑。`,
392
+ };
393
+ }
394
+ // 顶层 loop 的 do[] 步记复合 checkpoint(loopId@@迭代号@@stepId),
395
+ // resume 可恢复到迭代中间;更深嵌套不再下钻(descendIntoLoop=false)。
396
+ const innerCpFor = (iter) => cp?.descendIntoLoop
397
+ ? {
398
+ name: (id) => loopCheckpointName(step.id, iter, id),
399
+ descendIntoLoop: false,
400
+ }
401
+ : undefined;
237
402
  if (isWhileMode) {
238
403
  // ===== WHILE 循环模式 =====
239
404
  const asName = step.as ?? '_iter';
240
- let iterCount = 0;
405
+ let iterCount = resumeIterStart;
241
406
  let shouldBreak = false;
242
407
  while (iterCount < maxIter) {
243
408
  if (ctx.signal?.aborted) {
244
409
  yield { type: 'interrupted' };
245
- return true;
410
+ return 'halted';
246
411
  }
247
412
  // 注入迭代变量
248
413
  vars.push();
@@ -253,30 +418,58 @@ async function* runSteps(steps, vars, ctx, state) {
253
418
  ctx.history.length = 0;
254
419
  ctx.history.push(...baseHistorySnapshot);
255
420
  }
421
+ // 迭代序号对用户有真实意义(第几轮重做),title 步也保留,只换人话名。
256
422
  yield {
257
423
  type: 'plugin_progress',
258
424
  pluginId: WF_PLUGIN_ID,
259
425
  current: iterCount + 1,
260
426
  max: maxIter,
261
- message: `while-loop ${step.id} 迭代 ${iterCount + 1}/${maxIter}`,
427
+ message: step.title
428
+ ? `${interpolate(step.title, vars)}·第 ${iterCount + 1}/${maxIter} 轮`
429
+ : `while-loop ${step.id} 迭代 ${iterCount + 1}/${maxIter}`,
262
430
  };
263
- const childHalted = yield* runSteps(step.do ?? [], vars, ctx, state);
264
- vars.pop();
265
- if (childHalted) {
266
- // 区分 break 退出和 error/halt 退出
267
- // break 由 runSteps 内部处理,返回 true 但不 yield error
268
- // 这里统一返回 true(跳出外层循环)
269
- return true;
431
+ // 第 2+ 轮额外发持久 warning:上面的状态行 progress 会被首步 title 立即
432
+ // 覆盖(驻留一瞬),用户感知不到「循环重来了」——曾有 plan 工作流验收
433
+ // 打回静默重启,被用户误读成死循环。while 模式的多轮 = 重做/重试,
434
+ // 值得进 MessageList 留痕;for-each 的逐元素迭代是正常推进,不发。
435
+ if (iterCount > 0) {
436
+ yield {
437
+ type: 'warning',
438
+ message: `♻ 循环「${step.title ? interpolate(step.title, vars) : step.id}」进入第 ${iterCount + 1}/${maxIter} 轮`,
439
+ };
270
440
  }
441
+ const innerStart = pendingInnerStart;
442
+ pendingInnerStart = 0; // 仅 resume 后的首个迭代生效
443
+ const r = yield* runSteps(step.do ?? [], vars, ctx, state, innerCpFor(iterCount), innerStart);
444
+ vars.pop();
271
445
  iterCount++;
446
+ // halt(error/interrupt/onError:halt)→ 真正向上中止整个工作流。
447
+ if (r === 'halted')
448
+ return 'halted';
449
+ if (typeof r === 'object') {
450
+ // 带 target 且不是本循环 → 穿透上抛给外层循环
451
+ if (r.target !== undefined && r.target !== step.id)
452
+ return r;
453
+ if (r.signal === 'break') {
454
+ shouldBreak = true;
455
+ break;
456
+ }
457
+ // continue → 跳到下一轮迭代(什么都不做,自然进入下一轮)
458
+ }
272
459
  }
273
- await state.appendProgress(`✓ ${step.id} (while ×${iterCount}, 达到上限 ${maxIter})`);
460
+ const whyEnd = shouldBreak ? 'break 退出' : `达到上限 ${maxIter}`;
461
+ await state.appendProgress(`✓ ${step.id} (while ×${iterCount}, ${whyEnd})`);
274
462
  await state.writeVars(vars.snapshot());
463
+ // 循环完成记自身 checkpoint:resume 据此判定「循环已整体完成」(否则
464
+ // 中断在循环之后的步骤时,resume 会误把整条循环从头重跑)。
465
+ if (cp) {
466
+ await state.recordCheckpoint(cp.name(step.id), `while loop ${iterCount} iterations (${whyEnd})`, collectCaptured(step.capture, vars), vars.snapshot());
467
+ }
275
468
  yield {
276
469
  type: 'workflow_step_end',
277
470
  id: step.id,
278
471
  ok: true,
279
- output: `while loop ${iterCount} iterations (max=${maxIter})`,
472
+ output: `while loop ${iterCount} iterations (${whyEnd})`,
280
473
  };
281
474
  continue;
282
475
  }
@@ -290,21 +483,21 @@ async function* runSteps(steps, vars, ctx, state) {
290
483
  await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
291
484
  yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
292
485
  yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
293
- return true;
486
+ return 'halted';
294
487
  }
295
488
  if (!Array.isArray(arr)) {
296
489
  const errMsg = `loop over 必须求值为数组,得到 ${typeof arr}`;
297
490
  await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
298
491
  yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
299
492
  yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
300
- return true;
493
+ return 'halted';
301
494
  }
302
495
  const asName = step.as ?? 'item';
303
496
  let iterCount = 0;
304
- for (let idx = 0; idx < arr.length; idx++) {
497
+ for (let idx = resumeIterStart; idx < arr.length; idx++) {
305
498
  if (ctx.signal?.aborted) {
306
499
  yield { type: 'interrupted' };
307
- return true;
500
+ return 'halted';
308
501
  }
309
502
  vars.push();
310
503
  vars.set(asName, arr[idx]);
@@ -314,14 +507,26 @@ async function* runSteps(steps, vars, ctx, state) {
314
507
  ctx.history.length = 0;
315
508
  ctx.history.push(...baseHistorySnapshot);
316
509
  }
317
- const childHalted = yield* runSteps(step.do ?? [], vars, ctx, state);
510
+ const innerStart = pendingInnerStart;
511
+ pendingInnerStart = 0;
512
+ const r = yield* runSteps(step.do ?? [], vars, ctx, state, innerCpFor(idx), innerStart);
318
513
  vars.pop();
319
- if (childHalted)
320
- return true;
321
514
  iterCount++;
515
+ if (r === 'halted')
516
+ return 'halted';
517
+ if (typeof r === 'object') {
518
+ if (r.target !== undefined && r.target !== step.id)
519
+ return r; // 穿透上抛
520
+ if (r.signal === 'break')
521
+ break; // 跳出 for-each,循环后兄弟步继续
522
+ // continue → 下一个元素
523
+ }
322
524
  }
323
525
  await state.appendProgress(`✓ ${step.id} (loop ×${iterCount})`);
324
526
  await state.writeVars(vars.snapshot());
527
+ if (cp) {
528
+ await state.recordCheckpoint(cp.name(step.id), `loop ${iterCount} 次`, collectCaptured(step.capture, vars), vars.snapshot());
529
+ }
325
530
  yield {
326
531
  type: 'workflow_step_end',
327
532
  id: step.id,
@@ -330,287 +535,379 @@ async function* runSteps(steps, vars, ctx, state) {
330
535
  };
331
536
  continue;
332
537
  }
333
- // ---------- 控制流:pause ----------
538
+ // ---------- 控制流:pause(真 HITL 审批闸门) ----------
334
539
  //
335
- // 非交互模式(CLI -p / 测试)下无法真的暂停等待人工,直接当成 skipped。
336
- // 交互模式(TUI)需要 UI 层接住 step_skipped 把 reason 渲染出来 —— P0 已
337
- // 走通这条 UI 路径。step.prompt 用于把"暂停理由"传给上层呈现。
540
+ // 经 AskUserQuestion/askBridge 实现真停等:TUI 弹选择界面、用户拍板后继续;
541
+ // 非交互宿主(-p/gateway/测试)askBridge 自动选推荐项(继续执行)且 stderr
542
+ // 注记,绝不挂起——与 -p 单行 JSON 契约兼容。「等用户」骑在既有
543
+ // tool_call→tool_result 回路上(同 AskUserQuestion 工具),引擎零新通道。
338
544
  if (step.type === 'pause') {
339
545
  const msg = step.prompt
340
546
  ? interpolate(step.prompt, vars)
341
- : 'pause(非交互模式直接跳过)';
342
- await state.appendProgress(`- ${step.id} pause: ${msg}`);
343
- // P3:pause 在当前实现下**从不**真正等待人工(runner 是纯 AsyncGenerator,
344
- // 没有把用户输入回灌进来的通道)。把"静默放行"变"响亮放行"——发通用 warning
345
- // (-p 模式由 print.ts 落 stderr,权威通道)并镜像 plugin_progress(TUI 状态行可见),
346
- // 避免审批闸门被悄悄跳过。真·HITL pause 属 P4,本轮不做。
347
- const warnMsg = `⚠ pause "${step.id}" 在非交互模式被自动跳过,未真正等待人工确认——若这是审批闸门请勿依赖它。提示:${msg}`;
348
- yield { type: 'warning', message: warnMsg };
349
- yield {
350
- type: 'plugin_progress',
351
- pluginId: WF_PLUGIN_ID,
352
- current: 0,
353
- message: warnMsg,
354
- };
355
- yield {
356
- type: 'workflow_step_skipped',
357
- id: step.id,
358
- reason: `pause: ${msg}`,
359
- };
360
- yield {
361
- type: 'workflow_step_end',
362
- id: step.id,
363
- ok: true,
364
- output: 'paused (skipped in non-interactive mode)',
365
- };
366
- continue;
367
- }
368
- // ---------- 控制流:parallel ----------
369
- //
370
- // 每个 fork 独立 VarStack 副本 + 独立事件缓冲;Promise.all 并发执行。
371
- // 子 fork 的事件先收进 buffer,再按 fork 顺序一次性 yield,保 UI
372
- // 时间轴可读(并发 yield 会导致 UI 拿到乱序事件)。
373
- // 任一 fork halt → 整个 parallel halt。
374
- if (step.type === 'parallel') {
375
- const forks = step.fork ?? [];
376
- const results = await Promise.all(forks.map(async (forkStep) => {
377
- const forkVars = vars.fork();
378
- const events = [];
379
- let halted = false;
380
- try {
381
- halted = await collectStepsToBuffer([forkStep], forkVars, ctx, state, events);
382
- }
383
- catch (e) {
384
- halted = true;
385
- events.push({
386
- type: 'error',
387
- error: `fork ${forkStep.id} 抛异常: ${e.message}`,
388
- });
389
- }
390
- return {
391
- id: forkStep.id,
392
- events,
393
- vars: forkVars.snapshot(),
394
- halted,
395
- };
396
- }));
397
- // 顺序 yield 所有 fork 事件
398
- for (const r of results) {
399
- for (const ev of r.events)
400
- yield ev;
401
- }
402
- if (results.some((r) => r.halted)) {
403
- await state.appendProgress(`✗ ${step.id}: parallel: 至少一个 fork 失败`);
547
+ : '工作流暂停点:请确认是否继续执行后续步骤。';
548
+ await state.appendProgress(`- ${step.id} pause: 等待用户确认`);
549
+ // pause 会驻留状态行很久(等用户),title 步同样不带块内序号防误读。
550
+ if (step.title) {
404
551
  yield {
405
- type: 'workflow_step_end',
406
- id: step.id,
407
- ok: false,
408
- error: 'parallel: 至少一个 fork 失败',
552
+ type: 'plugin_progress',
553
+ pluginId: WF_PLUGIN_ID,
554
+ message: `⏸ ${interpolate(step.title, vars)}:等待用户确认…`,
409
555
  };
556
+ }
557
+ else {
410
558
  yield {
411
- type: 'error',
412
- error: `Workflow halted at step "${step.id}": parallel: 至少一个 fork 失败`,
559
+ type: 'plugin_progress',
560
+ pluginId: WF_PLUGIN_ID,
561
+ current: i + 1,
562
+ max: steps.length,
563
+ message: `⏸ 审批闸门 ${step.id}:等待用户确认…`,
413
564
  };
414
- return true;
415
565
  }
416
- // 聚合:raw.fork 是 [{ id, vars }, ...],capture 可走 fork.0.vars.xxx 这种点路径
417
- const aggResult = {
418
- raw: { fork: results.map((r) => ({ id: r.id, vars: r.vars })) },
419
- preview: `parallel: ${results.length} forks done`,
420
- };
421
- if (step.capture)
422
- bindCapture(step.capture, aggResult, vars);
423
- await state.appendProgress(`✓ ${step.id} (parallel ×${results.length})`);
424
- await state.writeVars(vars.snapshot());
425
- yield {
426
- type: 'workflow_step_end',
427
- id: step.id,
428
- ok: true,
429
- output: aggResult.preview,
430
- };
431
- continue;
432
- }
433
- // ---------- 控制流:vote ----------
434
- //
435
- // 对每个 input 数组项跑 N 个 voter(同 prompt + refuted: boolean),
436
- // refuted 票数 >= threshold 则视为驳回(item 不进 survived)。
437
- // 心理学定位:voter 倾向于 refuted=true(找漏洞),所以阈值算"驳回票数"。
438
- if (step.type === 'vote') {
439
- const voters_count = step.voters_count ?? 3;
440
- const threshold = step.threshold ?? Math.ceil(voters_count / 2);
441
- let inputArr;
566
+ const exec = ctx.executeTool ?? executeTool;
567
+ // 审批语义:明确选「继续」才放行;选「停止」、取消提问(ESC)、或自由输入
568
+ // 其它内容(既非继续也非停止 = 用户想先改点什么)都不放行(审批门上的
569
+ // 含糊 = 不批准,保守处理);桥本身异常才兜底放行(保持可用性)。
570
+ let approved = true;
571
+ let askFailed = false;
572
+ let freeText = '';
573
+ const freeTextMode = step.free_text ?? 'halt';
442
574
  try {
443
- inputArr = evalExpr(step.input ?? '[]', vars);
575
+ const r = await exec('AskUserQuestion', JSON.stringify({
576
+ questions: [
577
+ {
578
+ question: truncate(msg, 400),
579
+ header: '审批闸门',
580
+ options: [
581
+ { label: '继续执行 (推荐)', description: '放行后续步骤' },
582
+ {
583
+ label: '停止(保留现场)',
584
+ description: '中止本次运行;同任务重跑可从断点 resume',
585
+ },
586
+ ],
587
+ },
588
+ ],
589
+ }), ctx.signal);
590
+ // ⚠️ 只解析【答案行】("A1: …")——回执里 Q 行带着问题原文,
591
+ // 工作流的 pause 提示语若含「停止」字样(如 plan.yaml 的说明文字),
592
+ // 看整个 content 会把用户选的「继续」误判成停止(曾是真事故)。
593
+ const answerText = r.ok
594
+ ? r.content
595
+ .split('\n')
596
+ .map((l) => l.trim())
597
+ .filter((l) => /^A\d+[::]/.test(l))
598
+ .join('\n')
599
+ : '';
600
+ if (!r.ok || answerText.includes('停止')) {
601
+ approved = false;
602
+ }
603
+ else if (answerText.includes('继续')) {
604
+ approved = true;
605
+ }
606
+ else {
607
+ // 自由输入(「其他」逃生项):用户想先调整 → 不放行,把原话带进 halt 文案
608
+ freeText = answerText.replace(/^A\d+[::]\s*/, '');
609
+ approved = freeTextMode === 'approve';
610
+ }
444
611
  }
445
- catch (e) {
446
- const errMsg = `vote.input 求值失败: ${e.message}`;
447
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
448
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
449
- yield {
450
- type: 'error',
451
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
452
- };
453
- return true;
612
+ catch {
613
+ askFailed = true; // 桥不可用 → 放行 + 响亮警告(保持旧"不挂起"行为)
454
614
  }
455
- if (!Array.isArray(inputArr)) {
456
- const errMsg = `vote.input 必须求值为数组,得到 ${typeof inputArr}`;
457
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
458
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
615
+ if (askFailed) {
459
616
  yield {
460
- type: 'error',
461
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
617
+ type: 'warning',
618
+ message: `⚠ pause "${step.id}" 审批闸门不可用(提问桥异常),已自动放行。提示:${msg}`,
462
619
  };
463
- return true;
464
620
  }
465
- const survived = [];
466
- for (const item of inputArr) {
467
- if (ctx.signal?.aborted) {
468
- yield { type: 'interrupted' };
469
- return true;
470
- }
471
- const verdicts = await Promise.all(Array.from({ length: voters_count }, () => runVoter(step.rules ?? '', item, vars, ctx)));
472
- const refutedCount = verdicts.filter((v) => v.refuted === true).length;
473
- if (refutedCount < threshold) {
474
- survived.push(item);
475
- }
621
+ if (!approved) {
622
+ const why = freeText
623
+ ? `用户在审批闸门 "${step.id}" 给出指示「${truncate(freeText, 120)}」——未放行(现场已保留,按指示调整后重跑同目标可从断点续跑)`
624
+ : `用户在审批闸门 "${step.id}" 选择停止(现场已保留,重跑同目标可从断点续跑)`;
625
+ await state.appendProgress(`■ ${step.id}: ${why}`);
626
+ if (cp)
627
+ await state.recordFailure(step.id, why);
628
+ yield { type: 'workflow_step_end', id: step.id, ok: false, error: why };
629
+ yield { type: 'error', error: `Workflow halted at step "${step.id}": ${why}` };
630
+ return 'halted';
476
631
  }
477
- const voteResult = {
632
+ // 带补充要求的放行:自由输入并入贯穿目标(后续每步都看到,不止 capture 变量)
633
+ if (freeText)
634
+ await addGoalSupplement(ctx, state, freeText);
635
+ const pauseResult = {
478
636
  raw: {
479
- survived,
480
- total: inputArr.length,
481
- kept: survived.length,
637
+ result: freeText,
638
+ text: freeText,
639
+ approved: true,
640
+ free_text: freeText,
482
641
  },
483
- preview: `vote: ${survived.length}/${inputArr.length} survived (threshold=${threshold}/${voters_count})`,
642
+ preview: freeText
643
+ ? `审批通过:继续执行;补充要求:${truncate(freeText, 120)}`
644
+ : '审批通过:继续执行',
484
645
  };
485
646
  if (step.capture)
486
- bindCapture(step.capture, voteResult, vars);
487
- await state.appendProgress(`✓ ${step.id} (vote ${survived.length}/${inputArr.length})`);
647
+ bindCapture(step.capture, pauseResult, vars);
488
648
  await state.writeVars(vars.snapshot());
649
+ if (cp) {
650
+ await state.recordCheckpoint(cp.name(step.id), pauseResult.preview, collectCaptured(step.capture, vars), vars.snapshot());
651
+ }
652
+ await state.appendProgress(`✓ ${step.id} (pause: 已确认继续)`);
489
653
  yield {
490
654
  type: 'workflow_step_end',
491
655
  id: step.id,
492
656
  ok: true,
493
- output: voteResult.preview,
657
+ output: pauseResult.preview,
494
658
  };
495
659
  continue;
496
660
  }
497
- // ---------- 控制流:break(循环退出信号) ----------
661
+ // ---------- 控制流:break / continue(循环信号) ----------
498
662
  //
499
- // 仅在 loop 的 do[] 内有效。命中时跳出最近一层循环。
500
- // 通常配合 when 条件使用(如检测到 DONE 哨兵时跳出);
501
- // 无条件 break 极少用(等价于只跑一轮的循环)。
502
- if (step.type === 'break') {
503
- let shouldBreak = true;
663
+ // 仅在 loop 的 do[] 内有意义。break = 跳出循环;continue = 跳到下一轮迭代。
664
+ // 缺省作用于最近一层循环;带 target 时穿透中间层循环直达指定 id 的 loop
665
+ //(如审批反馈是方向性变化时,从审批小循环 continue 到外层 plan_loop 重启规划)。
666
+ // 通常配合 when 条件使用;游离信号(无外层循环 / target 不存在)冒泡到顶层
667
+ // 按正常完成处理,绝不当 halt。
668
+ if (step.type === 'break' || step.type === 'continue') {
669
+ let shouldFire = true;
504
670
  if (step.when) {
505
671
  try {
506
- shouldBreak = Boolean(evalExpr(step.when, vars));
672
+ shouldFire = Boolean(evalExpr(step.when, vars));
507
673
  }
508
674
  catch (e) {
509
- // when 表达式错误 → 不跳出,当 skipped 处理
510
- shouldBreak = false;
675
+ // when 表达式错误 → 不触发,当 skipped 处理
676
+ shouldFire = false;
511
677
  }
512
678
  }
513
- if (shouldBreak) {
514
- await state.appendProgress(`↵ ${step.id}: break(跳出循环)`);
515
- yield { type: 'workflow_step_end', id: step.id, ok: true, output: 'break' };
516
- return true; // ← 关键:返回 true 让外层 runSteps / loop 终止
679
+ if (shouldFire) {
680
+ const label = `${step.type}${step.target ? ` → ${step.target}` : ''}`;
681
+ await state.appendProgress(`↵ ${step.id}: ${label}`);
682
+ yield { type: 'workflow_step_end', id: step.id, ok: true, output: label };
683
+ return { signal: step.type, target: step.target };
517
684
  }
518
685
  // 条件不满足 → 当 skip 处理,继续下一步
519
686
  yield {
520
687
  type: 'workflow_step_skipped',
521
688
  id: step.id,
522
- reason: 'break 条件不满足',
689
+ reason: `${step.type} 条件不满足`,
523
690
  };
524
691
  continue;
525
692
  }
526
- // ---------- 普通动作 step:tool / llm / skill / assert ----------
527
- try {
528
- const result = yield* dispatchActionStep(step, vars, ctx);
529
- if (step.capture)
530
- bindCapture(step.capture, result, vars);
531
- await state.appendProgress(`✓ ${step.id} (${kind})`);
532
- await state.writeVars(vars.snapshot());
693
+ // ---------- 控制流:echo(确定性输出,不烧 LLM) ----------
694
+ //
695
+ // 把 message 插值后作为 text 事件输出给用户。适合工作流末尾的完成报告:
696
+ // 产物/裁决已在变量里时直接插值,省一次 LLM 调用且零幻觉。
697
+ if (step.type === 'echo') {
698
+ const missing = [];
699
+ const text = interpolate(step.message ?? '', vars, {
700
+ onMissing: (e) => missing.push(e),
701
+ });
702
+ if (missing.length > 0) {
703
+ yield {
704
+ type: 'warning',
705
+ message: `step "${step.id}": 模板变量未定义,已按空串插值:${[...new Set(missing)].map((m) => `\${${m}}`).join('、')}`,
706
+ };
707
+ }
708
+ yield { type: 'text', delta: text.endsWith('\n') ? text : `${text}\n` };
709
+ await state.appendProgress(`✓ ${step.id} (echo)`);
710
+ if (cp) {
711
+ await state.recordCheckpoint(cp.name(step.id), truncate(text, 200), {}, vars.snapshot());
712
+ }
533
713
  yield {
534
714
  type: 'workflow_step_end',
535
715
  id: step.id,
536
716
  ok: true,
537
- output: result.preview,
717
+ output: truncate(text, 200),
538
718
  };
719
+ continue;
539
720
  }
540
- catch (e) {
541
- const errMsg = e.message ?? String(e);
542
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
543
- // onError:'recover' —— 不立刻 halt,交给有界恢复 agent 随机应变把这一步救回来。
544
- // 「确定性工作流 × TAOR」的结合点:happy path 走刚性 DAG,撞上预料之外的就地
545
- // 降级为 agent 自适应;救回则继续 happy path,救不回才 honest halt。
546
- if (step.onError === 'recover') {
547
- yield {
548
- type: 'warning',
549
- message: `step "${step.id}" 失败:${errMsg};尝试自适应恢复…`,
550
- };
721
+ // ---------- 普通动作 step:tool / llm / skill / agent / assert / verify ----------
722
+ //
723
+ // in-run retry(self_heal):失败先确定性重跑同一步,至多 maxRetries 次(retry 先于
724
+ // onError:recover)。maxRetries = step.max_retries ?? def.self_heal.max_retries(runWorkflow
725
+ // 注入到 ctx.selfHealMaxRetries)?? 0。语义:max_retries 是「额外重试次数」,总尝试 = 1 + maxRetries。
726
+ const maxRetries = step.max_retries ?? ctx.selfHealMaxRetries ?? 0;
727
+ let attempt = 0;
728
+ while (true) {
729
+ try {
730
+ const result = yield* dispatchActionStep(step, vars, ctx);
731
+ if (step.capture)
732
+ bindCapture(step.capture, result, vars);
733
+ // goal_supplement:本步产出作为用户补充要求贯穿后续每一步(如 stage0 澄清结论)
734
+ if (step.goal_supplement) {
735
+ const t = typeof result.raw.text === 'string' && result.raw.text.trim()
736
+ ? result.raw.text
737
+ : String(result.raw.result ?? result.preview ?? '');
738
+ await addGoalSupplement(ctx, state, t);
739
+ }
740
+ await state.appendProgress(`✓ ${step.id} (${kind})`);
741
+ await state.writeVars(vars.snapshot());
742
+ // checkpoint:记录范围见 CpScope(顶层步 + 顶层 loop 的 do[] 一层)。
743
+ if (cp) {
744
+ await state.recordCheckpoint(cp.name(step.id), result.preview, collectCaptured(step.capture, vars), vars.snapshot());
745
+ }
551
746
  yield {
552
- type: 'plugin_progress',
553
- pluginId: WF_PLUGIN_ID,
554
- current: i + 1,
555
- max: steps.length,
556
- message: `↻ 恢复 ${step.id}…`,
747
+ type: 'workflow_step_end',
748
+ id: step.id,
749
+ ok: true,
750
+ output: result.preview,
557
751
  };
558
- const rec = yield* runRecovery(step, vars, ctx, errMsg);
559
- if (ctx.signal?.aborted) {
560
- yield { type: 'interrupted' };
561
- return true;
752
+ break; // 成功 → 退出 retry 循环,进入下一步
753
+ }
754
+ catch (e) {
755
+ const errMsg = e.message ?? String(e);
756
+ // in-run retry:还有重试额度且未中断 → 重跑同一步(不动 onError)。
757
+ if (attempt < maxRetries && !ctx.signal?.aborted) {
758
+ attempt++;
759
+ // 退避等待(线性):普通 LLM/网络类错误用 retry_delay_ms。
760
+ // maxTurns 截断默认保持旧行为立即续跑;长文档/PPT 类工作流可显式配置
761
+ // max_turns_retry_delay_ms,在模型端高峰期给下一次续作留出恢复窗口。
762
+ const isMaxTurns = isMaxTurnsCutoff(errMsg);
763
+ const delayBase = isMaxTurns
764
+ ? (ctx.maxTurnsRetryDelayMs ?? 0)
765
+ : (ctx.retryDelayMs ?? 0);
766
+ const delayMs = delayBase * attempt;
767
+ const retryKind = isMaxTurns ? '轮数预算用尽,可续跑截断' : '失败';
768
+ const retryProgress = isMaxTurns
769
+ ? `↻ ${step.id}: 阶段轮数预算用尽,正在续跑 ${attempt}/${maxRetries}${delayMs > 0 ? `(${delayMs}ms 后)` : ''}`
770
+ : `↻ ${step.id}: ${retryKind},重试 ${attempt}/${maxRetries}${delayMs > 0 ? `(${delayMs}ms 后)` : ''}(${errMsg})`;
771
+ await state.appendProgress(retryProgress);
772
+ yield {
773
+ type: 'warning',
774
+ message: isMaxTurns
775
+ ? `step "${step.id}" 阶段轮数预算用尽,正在续跑 ${attempt}/${maxRetries}${delayMs > 0 ? `(${delayMs}ms 后)` : ''}…`
776
+ : `step "${step.id}" 失败:${errMsg};${delayMs > 0 ? `${delayMs}ms 后` : ''}重试 ${attempt}/${maxRetries}…`,
777
+ };
778
+ yield {
779
+ type: 'plugin_progress',
780
+ pluginId: WF_PLUGIN_ID,
781
+ current: i + 1,
782
+ max: steps.length,
783
+ message: isMaxTurns
784
+ ? `↻ ${step.id} 阶段轮数预算用尽,正在续跑 ${attempt}/${maxRetries}…`
785
+ : `↻ 重试 ${step.id} ${attempt}/${maxRetries}…`,
786
+ };
787
+ if (delayMs > 0)
788
+ await sleepInterruptible(delayMs, ctx.signal);
789
+ continue; // 重跑同一步
562
790
  }
563
- // 验证闸门:只有恢复 agent 真正交付(rec.ok)才绑 capture 走下去;
564
- // 显式放弃 / 产出不可信(rec.ok=false)则带原因 honest halt,绝不盲绑脏值。
565
- if (rec.ok) {
566
- if (step.capture)
567
- bindCapture(step.capture, rec.value, vars);
568
- await state.appendProgress(`⚠→✓ ${step.id} recovered`);
569
- await state.writeVars(vars.snapshot());
791
+ await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
792
+ // onError:'recover' —— retry 用尽仍失败,交给有界恢复 agent 随机应变把这一步救回来。
793
+ // 「确定性工作流 × TAOR」的结合点:happy path 走刚性 DAG,撞上预料之外的就地
794
+ // 降级为 agent 自适应;救回则继续 happy path,救不回才 honest halt。
795
+ if (step.onError === 'recover') {
796
+ yield {
797
+ type: 'warning',
798
+ message: `step "${step.id}" 失败:${errMsg};尝试自适应恢复…`,
799
+ };
800
+ yield {
801
+ type: 'plugin_progress',
802
+ pluginId: WF_PLUGIN_ID,
803
+ current: i + 1,
804
+ max: steps.length,
805
+ message: `↻ 恢复 ${step.id}…`,
806
+ };
807
+ const rec = yield* runRecovery(step, vars, ctx, errMsg);
808
+ if (ctx.signal?.aborted) {
809
+ yield { type: 'interrupted' };
810
+ return 'halted';
811
+ }
812
+ // 验证闸门:只有恢复 agent 真正交付(rec.ok)才绑 capture 走下去;
813
+ // 显式放弃 / 产出不可信(rec.ok=false)则带原因 honest halt,绝不盲绑脏值。
814
+ if (rec.ok) {
815
+ if (step.capture)
816
+ bindCapture(step.capture, rec.value, vars);
817
+ await state.appendProgress(`⚠→✓ ${step.id} recovered`);
818
+ await state.writeVars(vars.snapshot());
819
+ if (cp) {
820
+ await state.recordCheckpoint(cp.name(step.id), rec.value.preview, collectCaptured(step.capture, vars), vars.snapshot());
821
+ }
822
+ yield {
823
+ type: 'workflow_step_end',
824
+ id: step.id,
825
+ ok: true,
826
+ output: rec.value.preview,
827
+ };
828
+ break; // 救回 → 进入下一步
829
+ }
830
+ const combined = `${errMsg}(自适应恢复未成功:${rec.reason})`;
831
+ if (cp)
832
+ await state.recordFailure(step.id, combined);
570
833
  yield {
571
834
  type: 'workflow_step_end',
572
835
  id: step.id,
573
- ok: true,
574
- output: rec.value.preview,
836
+ ok: false,
837
+ error: combined,
575
838
  };
576
- continue;
839
+ yield {
840
+ type: 'error',
841
+ error: `Workflow halted at step "${step.id}": ${combined}`,
842
+ };
843
+ return 'halted';
577
844
  }
578
- const combined = `${errMsg}(自适应恢复未成功:${rec.reason})`;
845
+ if (step.onError === 'continue') {
846
+ // 降级时给 capture 变量绑【明确占位文本】而非留空:空串静默内插进下游
847
+ // prompt(如「已知需求分析:\n\n」)是更大的最小惊讶违例——模型看到
848
+ // 空段会困惑。占位前缀「(上游步骤 」机器可识别;continue 本就是作者
849
+ // 显式选择「无此内容也继续」,需要严格闸门的步骤应配 onError:halt 或 assert。
850
+ if (step.capture) {
851
+ const placeholder = `(上游步骤 ${step.id} 失败已降级(onError:continue),无内容——` +
852
+ `请基于用户原始目标自行推断。错误:${errMsg.length > 120 ? `${errMsg.slice(0, 120)}…` : errMsg})`;
853
+ for (const varName of Object.values(normalizeCapture(step.capture))) {
854
+ vars.setGlobal(varName, placeholder);
855
+ }
856
+ }
857
+ // 降级此前完全静默(仅 progress.md 有 ✗ 行)——补一条 warning 可见化
858
+ yield {
859
+ type: 'warning',
860
+ message: `step "${step.id}" 失败已降级(onError:continue):${errMsg}`,
861
+ };
862
+ yield {
863
+ type: 'workflow_step_end',
864
+ id: step.id,
865
+ ok: false,
866
+ error: errMsg,
867
+ };
868
+ break; // 跳过本步 → 进入下一步
869
+ }
870
+ const finalErrMsg = isMaxTurnsCutoff(errMsg)
871
+ ? `${errMsg};重试额度已用尽,请拆分该步骤或提高 maxTurns。`
872
+ : errMsg;
873
+ if (cp)
874
+ await state.recordFailure(step.id, finalErrMsg);
579
875
  yield {
580
876
  type: 'workflow_step_end',
581
877
  id: step.id,
582
878
  ok: false,
583
- error: combined,
879
+ error: finalErrMsg,
584
880
  };
585
881
  yield {
586
882
  type: 'error',
587
- error: `Workflow halted at step "${step.id}": ${combined}`,
883
+ error: `Workflow halted at step "${step.id}": ${finalErrMsg}`,
588
884
  };
589
- return true;
885
+ return 'halted';
590
886
  }
591
- if (step.onError === 'continue') {
592
- yield {
593
- type: 'workflow_step_end',
594
- id: step.id,
595
- ok: false,
596
- error: errMsg,
597
- };
598
- continue;
599
- }
600
- yield {
601
- type: 'workflow_step_end',
602
- id: step.id,
603
- ok: false,
604
- error: errMsg,
605
- };
606
- yield {
607
- type: 'error',
608
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
609
- };
610
- return true;
611
887
  }
612
888
  }
613
- return false;
889
+ return 'completed';
890
+ }
891
+ /**
892
+ * 聚合工作流【声明】的产物路径(相对 cwd):def.artifacts + 所有 step.artifacts(含嵌套 then/else/do)。
893
+ * 通用:由 yaml 的 `artifacts:` 声明驱动,runner 不硬编码任何具体文件名(如 PLAN.md)。
894
+ * 用途:new-task 检测时清旧任务残留产物、completed 时报实际产物。
895
+ */
896
+ function collectDeclaredArtifacts(def) {
897
+ const out = new Set();
898
+ for (const a of def.artifacts ?? [])
899
+ out.add(a);
900
+ const walk = (steps) => {
901
+ for (const s of steps ?? []) {
902
+ for (const a of s.artifacts ?? [])
903
+ out.add(a);
904
+ walk(s.then);
905
+ walk(s.else);
906
+ walk(s.do);
907
+ }
908
+ };
909
+ walk(def.steps);
910
+ return [...out];
614
911
  }
615
912
  /**
616
913
  * 运行一个 workflow。
@@ -634,26 +931,148 @@ export async function* runWorkflow(def, inputs, ctx) {
634
931
  // 注入 RunContext.goalAnchor,让 agent/llm/skill 步自动前置到每步 prompt。
635
932
  // 提取策略:优先 "goal" 命名的输入 → 其次第一个 required string 输入。
636
933
  runCtx.goalAnchor = buildGoalAnchor(def.inputs ?? [], inputs);
637
- const state = new WorkflowState(getWorkingDir());
638
- await state.init(def.name, inputs, def.steps);
934
+ // self_heal:把全局 max_retries 默认透传给每步的 in-run retry(step.max_retries 可覆盖)。
935
+ runCtx.selfHealMaxRetries = def.self_heal?.max_retries;
936
+ runCtx.retryDelayMs = def.self_heal?.retry_delay_ms;
937
+ runCtx.maxTurnsRetryDelayMs = def.self_heal?.max_turns_retry_delay_ms;
938
+ // 声明产物清单:state.init 用于 new-task 清理/completed 报产物;
939
+ // 挂到 runCtx 供 tool 步覆盖判定(声明产物自动 pre-Read 解 Write 守卫)。
940
+ const declaredArtifacts = collectDeclaredArtifacts(def);
941
+ runCtx.declaredArtifacts = declaredArtifacts;
942
+ // 任务身份语义仲裁器(hash 不等 + 旧任务未完成时的二审,见 taskIdentity.ts):
943
+ // 短语快路径 → LLM 单轮仲裁 → AskUserQuestion → 兜底 new-task。
944
+ // init 非 generator,仲裁过程的人话记录收进 notes,init 返回后统一 yield。
945
+ const arbitrationNotes = [];
946
+ const state = new WorkflowState(getWorkingDir(), def.name);
947
+ const resume = await state.init(def.name, inputs, def.steps, def.self_heal?.backoff, declaredArtifacts, buildTaskArbitrator({
948
+ provider: runCtx.provider,
949
+ chat: runCtx.chat,
950
+ executeTool: runCtx.executeTool,
951
+ signal: runCtx.signal,
952
+ notes: arbitrationNotes,
953
+ }));
639
954
  yield {
640
955
  type: 'workflow_start',
641
956
  name: def.name,
642
957
  totalSteps: def.steps.length,
643
958
  };
644
- // P1 TUI 可见:开工时给 LiveArea 状态行一条 plugin_progress(0/total)。
959
+ // 仲裁过程可见化(命中延续短语 / LLM 判定 / 用户确认 / 兜底)
960
+ for (const n of arbitrationNotes) {
961
+ yield { type: 'warning', message: n };
962
+ }
963
+ // 同任务此前已成功完成(goal hash 匹配 + status=done)→ 不重跑,报产物后正常收尾。
964
+ // 成功运行会 archive() 把状态目录改名 .done,init() 回退读 .done 检测到 completed。
965
+ if (resume.mode === 'completed') {
966
+ const arts = resume.artifacts?.length ? resume.artifacts.join(', ') : '(无记录)';
967
+ yield {
968
+ type: 'text',
969
+ delta: `✓ 同名任务此前已完成(产物:${arts})。如需重跑请先 /new 清理工作流状态。\n`,
970
+ };
971
+ yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
972
+ return;
973
+ }
974
+ // 定义结构变更可见化:同任务但 yaml 改过结构 → 旧断点作废、从头开始。
975
+ // 必须明示——曾有用户在 yaml 重构后同 goal 重跑,看到「从第 2 步续跑」
976
+ // 困惑是 bug 还是残留;作废旧断点时不说原因同样会被当成 bug。
977
+ if (resume.defChanged) {
978
+ yield {
979
+ type: 'warning',
980
+ message: '♻ 工作流定义已变更(步骤结构与旧断点不再匹配),旧执行现场已作废,本次从头开始执行。',
981
+ };
982
+ }
983
+ // fresh/new-task 启动清理可见化:全新任务开始前已删掉的残留产物(上次任务
984
+ // 的声明产物 + /new 后无主孤儿)。用户约定:全新 plan 一律从干净状态开始;
985
+ // 删除范围仅限本工作流声明的产物,同任务 resume 不清理。
986
+ if (resume.cleanedArtifacts?.length) {
987
+ yield {
988
+ type: 'warning',
989
+ message: `全新任务:已清理上次残留的旧产物 ${[...new Set(resume.cleanedArtifacts)].join('、')},从干净状态开始。`,
990
+ };
991
+ }
992
+ // 仲裁判定的延续 resume:本次 inputs 里的 goal 是「继续」这类延续指令,
993
+ // 不能拿它当目标——两处都要换回旧任务的真实目标:
994
+ // 1. goalAnchor:锚旧目标 + 新输入作为补充指令(如「继续,但改成 12 页」不丢失);
995
+ // 2. VarStack 的 inputs:yaml prompt 正文大量内插 ${inputs.goal}(业务上下文),
996
+ // 不替换的话重跑步骤会渲染成「用户的目标是:『继续』」——锚定与正文打架。
997
+ // goal 承载键用与 buildGoalAnchor/extractGoal 同一策略(通用,零具体 yaml 耦合);
998
+ // 磁盘 inputs.json 保持旧任务值不动,任务身份 hash 语义不受影响。
999
+ if (resume.mode === 'resume') {
1000
+ // 贯穿补充要求:上次运行累积的(pause 反馈 / 澄清结论)从状态目录恢复,
1001
+ // resume 后注入每步锚定不丢失。
1002
+ runCtx.goalSupplements = await state.readSupplements();
1003
+ }
1004
+ if (resume.mode === 'resume' && resume.prevGoal) {
1005
+ const goalKey = (def.inputs ?? []).find((d) => d.name === 'goal' && d.type === 'string')
1006
+ ?.name ??
1007
+ (def.inputs ?? []).find((d) => d.required && d.type === 'string')?.name;
1008
+ if (goalKey && typeof inputs[goalKey] === 'string') {
1009
+ vars.setGlobal('inputs', { ...inputs, [goalKey]: resume.prevGoal });
1010
+ }
1011
+ runCtx.goalAnchor = `> 🎯 用户原始目标:${truncateGoal(resume.prevGoal)}\n> ⚠️ 所有操作必须直接回应此目标,不可偏离或遗忘。`;
1012
+ // 新输入是带信息量的补充指令(非纯「继续」延续短语)→ 并入贯穿补充要求;
1013
+ // 经 goalSupplements 统一渲染(与 pause 反馈/澄清结论同通道),且去重防
1014
+ // 反复 resume 时同一句话累积成噪声。
1015
+ const supp = resume.supplementaryGoal;
1016
+ if (supp &&
1017
+ supp !== resume.prevGoal &&
1018
+ !isContinuationPhrase(supp) &&
1019
+ !(runCtx.goalSupplements ?? []).some((s) => s.startsWith(supp.slice(0, 240)))) {
1020
+ await addGoalSupplement(runCtx, state, supp);
1021
+ }
1022
+ }
1023
+ // resume:同任务中断后重跑 —— 回放已完成步的 capture 变量,从 startFrom 起跑。
1024
+ // ⚠️ 仅回放 VarStack 变量,不恢复 TAOR history(plan.yaml 全程用 capture 变量串联故可用;
1025
+ // 依赖隐式 history 的工作流 resume 后上下文较薄,见 types.ts 限制说明)。
1026
+ let startFrom = 0;
1027
+ if (resume.mode === 'resume' && ((resume.startFrom ?? 0) > 0 || resume.loopResume)) {
1028
+ startFrom = resume.startFrom ?? 0;
1029
+ if (resume.capturedVars && resume.completedStepIds) {
1030
+ for (const sid of resume.completedStepIds) {
1031
+ const caps = resume.capturedVars[sid];
1032
+ if (caps)
1033
+ for (const [k, v] of Object.entries(caps))
1034
+ vars.setGlobal(k, v);
1035
+ }
1036
+ }
1037
+ // 循环中断恢复:回放中断时刻的最近 vars 快照(含 setGlobal 的教训/裁决与
1038
+ // 迭代内已完成步的 capture)。inputs 不回放——goal 承载键已由上方仲裁逻辑处理。
1039
+ if (resume.loopResume?.lastVarsSnapshot) {
1040
+ for (const [k, v] of Object.entries(resume.loopResume.lastVarsSnapshot)) {
1041
+ if (k === 'inputs')
1042
+ continue;
1043
+ vars.setGlobal(k, v);
1044
+ }
1045
+ }
1046
+ const loopNote = resume.loopResume
1047
+ ? `,循环 ${resume.loopResume.loopId} 恢复到第 ${resume.loopResume.iteration + 1} 轮迭代`
1048
+ : '';
1049
+ const msg = `♻ 检测到同名未完成任务,从第 ${startFrom + 1} 步续跑(已回放 ${resume.completedStepIds?.length ?? 0} 个完成步变量${loopNote})。要全新开始请先 /new。`;
1050
+ yield { type: 'warning', message: msg };
1051
+ }
1052
+ // P1 TUI 可见:开工时给 LiveArea 状态行一条 plugin_progress(startFrom/total)。
645
1053
  yield {
646
1054
  type: 'plugin_progress',
647
1055
  pluginId: WF_PLUGIN_ID,
648
- current: 0,
1056
+ current: startFrom,
649
1057
  max: def.steps.length,
650
1058
  message: `▶ workflow ${def.name}`,
651
1059
  };
652
1060
  let succeeded = false;
653
1061
  try {
654
- const halted = yield* runSteps(def.steps, vars, runCtx, state);
655
- if (halted)
1062
+ const flow = yield* runSteps(def.steps, vars, runCtx, state, { name: (id) => id, descendIntoLoop: true }, startFrom, resume.mode === 'resume' ? resume.loopResume : undefined);
1063
+ // 'halted' → 失败/中断(error/interrupted 已 yield),保留现场不归档供排查与 resume;
1064
+ // 'completed' / 信号对象(顶层游离 break/continue)→ 正常完成。
1065
+ if (flow === 'halted')
656
1066
  return;
1067
+ const missingArtifacts = declaredArtifacts.filter((a) => !existsSync(resolve(runCtx.cwd, a)));
1068
+ if (missingArtifacts.length > 0) {
1069
+ const why = `工作流声明的产物缺失:${missingArtifacts.join('、')}` +
1070
+ `——所有步骤执行完毕但目标产物未落盘,不标记完成(保留现场供排查/续作)`;
1071
+ await state.recordFailure('<finalize>', why);
1072
+ yield { type: 'workflow_step_end', id: '<finalize>', ok: false, error: why };
1073
+ yield { type: 'error', error: `Workflow halted: ${why}` };
1074
+ return;
1075
+ }
657
1076
  // 完成态也给一条 plugin_progress(放在 workflow_done 之前,保证
658
1077
  // workflow_done 仍是事件流最后一条——e2e 测试依赖 events[last]===workflow_done)。
659
1078
  yield {
@@ -667,26 +1086,13 @@ export async function* runWorkflow(def, inputs, ctx) {
667
1086
  // agent/llm/skill 步已通过 runQuery(TAOR) 直接写入共享 ctx.history,
668
1087
  // 每步的完整对话上下文(user→assistant→tool→...)都自然积累在 history 中。
669
1088
  // 后续对话和用户调整都能看到完整的工作流执行过程。
1089
+ await state.markDone();
670
1090
  yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
671
1091
  succeeded = true;
672
1092
  }
673
1093
  finally {
674
- // 成功时归档(保留 vars.json/progress.md 供后续回溯);失败/中断时保留原状供排查
1094
+ // 成功时归档(保留 vars.json/progress.md/checkpoints 供后续回溯);失败/中断时保留原状供排查与 resume
675
1095
  if (succeeded)
676
1096
  await state.archive();
677
1097
  }
678
1098
  }
679
- /**
680
- * parallel fork 用:把 runSteps 的 yield 流收集到 buffer 数组,
681
- * 让外层 Promise.all 拿到完整事件后再顺序 yield。
682
- *
683
- * 注:for-await 无法读取 generator 的 return value(JS 限制),
684
- * 所以"是否 halted"靠"buffer 里有 error event"近似判断 —— 这符合
685
- * runSteps 的语义(halt 一定会 yield error / interrupted)。
686
- */
687
- async function collectStepsToBuffer(steps, vars, ctx, state, buffer) {
688
- for await (const ev of runSteps(steps, vars, ctx, state)) {
689
- buffer.push(ev);
690
- }
691
- return buffer.some((ev) => ev.type === 'error' || ev.type === 'interrupted');
692
- }