minimal-agent 0.8.2 → 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 (484) hide show
  1. package/README.md +14 -12
  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 +3 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +249 -51
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +198 -15
  23. package/plugins/workflow-runner/src/index.js +40 -8
  24. package/plugins/workflow-runner/src/jsonPayload.js +1 -1
  25. package/plugins/workflow-runner/src/loader.js +286 -72
  26. package/plugins/workflow-runner/src/multimodal.js +37 -0
  27. package/plugins/workflow-runner/src/recovery.js +49 -39
  28. package/plugins/workflow-runner/src/runner.js +802 -287
  29. package/plugins/workflow-runner/src/stepExecutors/agent.js +157 -39
  30. package/plugins/workflow-runner/src/stepExecutors/assert.js +16 -3
  31. package/plugins/workflow-runner/src/stepExecutors/llm.js +60 -111
  32. package/plugins/workflow-runner/src/stepExecutors/skill.js +47 -47
  33. package/plugins/workflow-runner/src/stepExecutors/tool.js +76 -10
  34. package/plugins/workflow-runner/src/stepExecutors/verify.js +27 -10
  35. package/plugins/workflow-runner/src/taskIdentity.js +296 -0
  36. package/plugins/workflow-runner/src/types.js +19 -9
  37. package/plugins/workflow-runner/src/workflowState.js +516 -33
  38. package/skills/adversarial-review/SKILL.md +0 -1
  39. package/skills/ai-image/SKILL.md +89 -0
  40. package/skills/ai-image/scripts/generate.mjs +497 -0
  41. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  42. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  43. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  44. package/skills/browser-automation/SKILL.md +47 -0
  45. package/skills/docx/SKILL.md +25 -3
  46. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  53. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  54. package/skills/docx/scripts/opc_check.py +253 -0
  55. package/skills/image-gen-openrouter/SKILL.md +10 -2
  56. package/skills/image-stats/SKILL.md +65 -0
  57. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  58. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  59. package/skills/image-stats/scripts/image_stats.py +368 -0
  60. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  61. package/skills/pptx/SKILL.md +123 -95
  62. package/skills/pptx/design.md +82 -0
  63. package/skills/pptx/examples/README.md +29 -0
  64. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  65. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  66. package/skills/pptx/pptxgenjs.md +41 -0
  67. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  68. package/skills/pptx/scripts/layout_lint.py +293 -0
  69. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  70. package/skills/pptx/scripts/opc_check.py +339 -0
  71. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  72. package/skills/pptx/svg.md +67 -0
  73. package/skills/tts-zh/SKILL.md +19 -2
  74. package/skills/ui-design/SKILL.md +158 -0
  75. package/skills/ui-design/claude_design.md +309 -0
  76. package/skills/ui-design/evals/evals.json +254 -0
  77. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  78. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  79. package/skills/ui-design/references/design-context.md +53 -0
  80. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  81. package/skills/ui-design/references/export-pptx.md +124 -0
  82. package/skills/ui-design/references/layout-composition.md +654 -0
  83. package/skills/ui-design/references/preview-feedback.md +37 -0
  84. package/skills/ui-design/references/source-image-layout.md +189 -0
  85. package/skills/ui-design/references/starter-components.md +78 -0
  86. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  87. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  88. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  89. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  90. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  91. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  92. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  93. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  94. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  95. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  96. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  97. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  98. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  99. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  100. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  140. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  141. package/skills/ui-design/scripts/assemble_material.py +217 -0
  142. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  143. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  144. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  145. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  146. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  147. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  148. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  149. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  150. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  151. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  152. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  153. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  154. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  155. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  156. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  157. package/skills/ui-design/scripts/design_style_check.py +397 -0
  158. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  159. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  160. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  161. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  162. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  163. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  164. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  165. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  166. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  167. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  168. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  169. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  170. package/skills/ui-design/scripts/layout_balance.py +881 -0
  171. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  172. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  173. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  174. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  175. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  176. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  177. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  178. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  179. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  180. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  181. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  182. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  183. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  184. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  185. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  186. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  187. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  188. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  195. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  196. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  205. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  206. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  221. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  222. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  225. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  226. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  239. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  240. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  248. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  249. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  261. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  262. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  275. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  292. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  293. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  316. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  324. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  325. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  328. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  329. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  337. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  338. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  339. package/skills/video-compose/SKILL.md +8 -1
  340. package/skills/web-crawler/SKILL.md +70 -0
  341. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  342. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  343. package/skills/xlsx/SKILL.md +12 -0
  344. package/skills/xlsx/scripts/opc_check.py +253 -0
  345. package/src/advisorState.js +22 -0
  346. package/src/askBridge.js +97 -3
  347. package/src/bootstrap/workingDir.js +12 -1
  348. package/src/cli/args.js +17 -1
  349. package/src/cli/configMenu.js +236 -0
  350. package/src/cli/configWizard.js +7 -5
  351. package/src/cli/print.js +37 -4
  352. package/src/cli/streamJson.js +77 -24
  353. package/src/cli/wizards/advisorWizard.js +147 -0
  354. package/src/cli/wizards/fieldForm.js +149 -0
  355. package/src/cli/wizards/imageWizard.js +60 -0
  356. package/src/cli/wizards/openRouterWizard.js +23 -0
  357. package/src/cli/wizards/providerPresets.js +95 -0
  358. package/src/cli/wizards/providerWizard.js +219 -0
  359. package/src/cli/wizards/searchWizard.js +23 -0
  360. package/src/cli/wizards/ttsWizard.js +34 -0
  361. package/src/config/configFile.js +441 -83
  362. package/src/config/configService.js +446 -0
  363. package/src/config.js +24 -64
  364. package/src/context/agentHome.js +9 -0
  365. package/src/context/archive.js +7 -2
  366. package/src/context/compact.js +8 -5
  367. package/src/context/contextLock.js +81 -0
  368. package/src/context/persistContext.js +30 -11
  369. package/src/context/reactiveCompact.js +4 -2
  370. package/src/context/recentDirs.js +21 -61
  371. package/src/context/sessionPath.js +97 -23
  372. package/src/context/sessionRegistry.js +303 -24
  373. package/src/context/tokenCounter.js +18 -0
  374. package/src/context/workspaceIndex.js +731 -0
  375. package/src/infra/verification.js +28 -15
  376. package/src/llm/client.js +394 -159
  377. package/src/llm/providers/anthropicFormat.js +241 -0
  378. package/src/llm/providers/openaiFormat.js +190 -0
  379. package/src/loop.js +219 -32
  380. package/src/main.js +61 -11
  381. package/src/messageQueue.js +97 -0
  382. package/src/multimodal.js +203 -0
  383. package/src/plugin-sdk.js +15 -0
  384. package/src/plugins/commandRouter.js +3 -0
  385. package/src/plugins/hookEngine.js +5 -1
  386. package/src/plugins/pluginLifecycle.js +28 -0
  387. package/src/plugins/pluginLoader.js +2 -1
  388. package/src/plugins/pluginRunner.js +49 -21
  389. package/src/plugins/transcript.js +4 -3
  390. package/src/prompts/pluginList.js +5 -5
  391. package/src/prompts/projectInstructions.js +3 -3
  392. package/src/prompts/promptAssembly.js +133 -144
  393. package/src/prompts/skillList.js +12 -22
  394. package/src/prompts/system.js +0 -1
  395. package/src/session/runAgentTurn.js +70 -0
  396. package/src/session/runTurn.js +41 -6
  397. package/src/toolGate.js +17 -0
  398. package/src/tools/advisor/advisor.js +379 -0
  399. package/src/tools/agent/agent.js +555 -0
  400. package/src/tools/agent/agentTypes.js +59 -0
  401. package/src/tools/agent/coordinator.js +93 -0
  402. package/src/tools/agent/worktree.js +270 -0
  403. package/src/tools/ask/ask.js +7 -2
  404. package/src/tools/bash/bash.js +112 -18
  405. package/src/tools/download/download.js +144 -0
  406. package/src/tools/edit/edit.js +1 -0
  407. package/src/tools/edit/multi-edit.js +1 -0
  408. package/src/tools/genimage/genimage.js +242 -0
  409. package/src/tools/genspeech/genspeech.js +67 -0
  410. package/src/tools/glob/glob.js +1 -0
  411. package/src/tools/grep/grep.js +3 -0
  412. package/src/tools/grep/rgPath.js +2 -0
  413. package/src/tools/index.js +25 -2
  414. package/src/tools/read/read.js +1 -0
  415. package/src/tools/shared/killTree.js +52 -0
  416. package/src/tools/toolProgressBridge.js +48 -0
  417. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  418. package/src/tools/webbrowser/browser.js +59 -42
  419. package/src/tools/webbrowser/browserControl.js +118 -0
  420. package/src/tools/webbrowser/browserDaemon.js +97 -0
  421. package/src/tools/webbrowser/browserService.js +27 -0
  422. package/src/tools/webbrowser/browserStream.js +119 -0
  423. package/src/tools/webbrowser/webbrowser.js +699 -130
  424. package/src/tools/webfetch/webfetch.js +1 -0
  425. package/src/tools/websearch/websearch.js +134 -51
  426. package/src/tools/write/write.js +1 -0
  427. package/src/ui/App.js +249 -22
  428. package/src/ui/AskQuestion.js +29 -10
  429. package/src/ui/InputBox.js +104 -12
  430. package/src/ui/LiveArea.js +112 -19
  431. package/src/ui/MessageList.js +100 -14
  432. package/src/ui/ModelPicker.js +56 -0
  433. package/src/ui/QueuedMessagesBanner.js +14 -0
  434. package/src/ui/Root.js +18 -10
  435. package/src/ui/SessionPicker.js +102 -0
  436. package/src/ui/StatusLine.js +13 -4
  437. package/src/ui/WorkspacePicker.js +55 -0
  438. package/src/ui/figures.js +44 -0
  439. package/src/ui/hooks/askSlot.js +82 -0
  440. package/src/ui/hooks/useChat.js +358 -64
  441. package/src/ui/hooks/usePasteHandler.js +11 -1
  442. package/src/ui/hooks/useTextBuffer.js +1 -3
  443. package/src/ui/liveViewport.js +5 -1
  444. package/src/ui/markdown.js +206 -0
  445. package/src/ui/textBuffer.js +0 -7
  446. package/src/ui/theme.js +32 -0
  447. package/src/utils/debugLog.js +0 -4
  448. package/src/utils/resourcePaths.js +2 -2
  449. package/src/utils/stripThink.js +50 -0
  450. package/src/web/browserBridge.js +447 -0
  451. package/src/web/protocol.js +19 -0
  452. package/workflows/adaptive-research.yaml +13 -5
  453. package/workflows/book-review-short.yaml +36 -10
  454. package/workflows/e2e-write-greet.yaml +6 -2
  455. package/workflows/illustrate.yaml +227 -0
  456. package/workflows/plan.yaml +460 -179
  457. package/workflows/ppt.yaml +1659 -0
  458. package/workflows/ralph-loop.yaml +199 -0
  459. package/workflows/schema.json +81 -23
  460. package/workflows/webppt.yaml +496 -0
  461. package/workflows/word.yaml +710 -0
  462. package/workflows/youtube-shorts.yaml +46 -15
  463. package/MINIMAL-TRACE.md +0 -77
  464. package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
  465. package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
  466. package/plugins/ralph-wiggum/README.md +0 -197
  467. package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
  468. package/plugins/ralph-wiggum/commands/help.md +0 -137
  469. package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
  470. package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
  471. package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
  472. package/plugins/ralph-wiggum/plugin.js +0 -280
  473. package/plugins/ralph-wiggum/src/goalState.js +0 -258
  474. package/plugins/ralph-wiggum/src/sentinels.js +0 -76
  475. package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
  476. package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
  477. package/plugins/workflow-runner/src/miniReAct.js +0 -223
  478. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  479. package/skills/config/SKILL.md +0 -123
  480. package/skills/frontend-design/LICENSE.txt +0 -177
  481. package/skills/frontend-design/SKILL.md +0 -42
  482. package/src/ui/ProgressPanel.js +0 -54
  483. package/src/ui/StreamingBlock.js +0 -11
  484. package/src/ui/ToolStatus.js +0 -11
@@ -11,29 +11,31 @@
11
11
  *
12
12
  * 支持的 step kind(loader 已校验):
13
13
  * - tool / llm / assert —— 原子动作(execXxxStep 返回 StepResult)
14
- * - agent / skill —— 有界 TAOR:miniReAct(agent) / 外部 SKILL.md(skill);
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>"
@@ -41,7 +43,78 @@ import { WorkflowState } from './workflowState.js';
41
43
  * (-p 非交互模式另由 src/cli/print.ts 把通用 warning 事件落 stderr。)
42
44
  */
43
45
  const WF_PLUGIN_ID = 'workflow-runner';
46
+ /**
47
+ * 从工作流 inputs 定义 + 实际值中提取用户的原始目标,
48
+ * 构建紧凑的目标锚定字符串。
49
+ *
50
+ * 提取策略:
51
+ * 1. 优先取名为 "goal" 的输入(plan.yaml / ralph-loop.yaml 等通用约定)
52
+ * 2. 其次取第一个 required: true 的 string 类型输入
53
+ * 3. 都没有 → 返回空字符串(executor 自动跳过)
54
+ *
55
+ * 锚定字符串格式紧凑(~60-150 字符),注入每步 prompt 前置,
56
+ * 确保 LLM 在任何步骤都能"看到"用户原始目标,避免长上下文稀释。
57
+ */
58
+ function buildGoalAnchor(inputDefs, inputValues) {
59
+ let goalValue;
60
+ // 策略 1:优先 "goal"
61
+ const goalInput = inputDefs.find((d) => d.name === 'goal' && d.type === 'string');
62
+ if (goalInput) {
63
+ const v = inputValues[goalInput.name];
64
+ if (typeof v === 'string' && v.trim()) {
65
+ goalValue = v.trim();
66
+ }
67
+ }
68
+ // 策略 2:fallback 第一个 required string
69
+ if (!goalValue) {
70
+ const firstRequired = inputDefs.find((d) => d.required && d.type === 'string');
71
+ if (firstRequired) {
72
+ const v = inputValues[firstRequired.name];
73
+ if (typeof v === 'string' && v.trim()) {
74
+ goalValue = v.trim();
75
+ }
76
+ }
77
+ }
78
+ if (!goalValue)
79
+ return '';
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);
114
+ }
44
115
  function stepKind(step) {
116
+ if (step.type === 'break')
117
+ return 'break';
45
118
  if (step.type)
46
119
  return step.type;
47
120
  if (step.tool)
@@ -65,20 +138,41 @@ function readPath(root, path) {
65
138
  return cur;
66
139
  }
67
140
  function bindCapture(captureMap, result, vars) {
68
- for (const [resultField, varName] of Object.entries(captureMap)) {
141
+ for (const [resultField, varName] of Object.entries(normalizeCapture(captureMap))) {
69
142
  const value = resultField.includes('.')
70
143
  ? readPath(result.raw, resultField)
71
144
  : result.raw[resultField];
72
145
  vars.set(varName, value);
73
146
  }
74
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
+ }
75
169
  /**
76
170
  * 分发"动作"型 step(tool / agent / llm / skill / assert)到对应 executor。
77
- * 控制流 step(branch / loop / pause / parallel / vote)由 runSteps 直接处理,不走这里。
171
+ * 控制流 step(branch / loop / pause 等)由 runSteps 直接处理,不走这里。
78
172
  */
79
173
  async function* dispatchActionStep(step, vars, ctx) {
80
174
  if (step.type === 'assert')
81
- return execAssertStep(step, vars);
175
+ return execAssertStep(step, vars, ctx);
82
176
  if (step.type === 'verify')
83
177
  return await execVerifyStep(step, vars);
84
178
  if (step.tool)
@@ -92,17 +186,34 @@ async function* dispatchActionStep(step, vars, ctx) {
92
186
  throw new Error(`step ${step.id}: 无可执行字段(tool/agent/llm/skill/assert/verify)`);
93
187
  }
94
188
  /**
95
- * 递归执行 steps[]。返回 true 表示该 block 已 halt(上游应中止)。
189
+ * 递归执行 steps[]。返回 StepFlow(completed / halted / break|continue 信号,见上方定义)。
96
190
  *
97
191
  * yields 混合事件:WorkflowEvent + LoopEvent(skill 子循环转发 / error / interrupted)。
98
192
  * index/total 字段相对**当前 block**,嵌套 block 自有计数(branch.then 内 3 步就报 total=3)。
99
193
  */
100
- 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) {
101
203
  for (let i = 0; i < steps.length; i++) {
102
204
  const step = steps[i];
103
205
  if (ctx.signal?.aborted) {
104
206
  yield { type: 'interrupted' };
105
- 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;
106
217
  }
107
218
  // when 条件
108
219
  if (step.when) {
@@ -122,7 +233,7 @@ async function* runSteps(steps, vars, ctx, state) {
122
233
  type: 'error',
123
234
  error: `Workflow halted at step "${step.id}": ${errMsg}`,
124
235
  };
125
- return true;
236
+ return 'halted';
126
237
  }
127
238
  if (!pass) {
128
239
  yield {
@@ -136,23 +247,39 @@ async function* runSteps(steps, vars, ctx, state) {
136
247
  }
137
248
  const kind = stepKind(step);
138
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;
139
255
  yield {
140
256
  type: 'workflow_step_start',
141
257
  id: step.id,
142
258
  kind,
143
259
  index: i,
144
260
  total: steps.length,
261
+ title: interpolatedTitle,
145
262
  };
146
- // P1 TUI 可见:把私有 workflow_step_start 镜像成框架通用 plugin_progress,
147
- // LiveArea 状态行渲染成 "[plugin:workflow-runner] i/total step <id> (<kind>)"。
148
- // (嵌套 block 的 i/total 相对当前 block,足够给"跑到哪一步"的实时反馈。)
149
- yield {
150
- type: 'plugin_progress',
151
- pluginId: WF_PLUGIN_ID,
152
- current: i + 1,
153
- max: steps.length,
154
- message: `step ${step.id} (${kind})`,
155
- };
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
+ }
156
283
  // ---------- 控制流:branch ----------
157
284
  if (step.type === 'branch') {
158
285
  let cond = false;
@@ -164,13 +291,14 @@ async function* runSteps(steps, vars, ctx, state) {
164
291
  await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
165
292
  yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
166
293
  yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
167
- return true;
294
+ return 'halted';
168
295
  }
169
296
  const branch = cond ? step.then : step.else;
170
297
  if (Array.isArray(branch) && branch.length > 0) {
171
- const childHalted = yield* runSteps(branch, vars, ctx, state);
172
- if (childHalted)
173
- return true;
298
+ // branch 对 break 透明:子块 break/halt 都原样上抛给最近的循环 / 顶层。
299
+ const r = yield* runSteps(branch, vars, ctx, state);
300
+ if (r !== 'completed')
301
+ return r;
174
302
  }
175
303
  await state.appendProgress(`✓ ${step.id} (branch=${cond})`);
176
304
  await state.writeVars(vars.snapshot());
@@ -182,8 +310,170 @@ async function* runSteps(steps, vars, ctx, state) {
182
310
  };
183
311
  continue;
184
312
  }
185
- // ---------- 控制流:loop ----------
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
+ }
356
+ // ---------- 控制流:loop(for-each 或 while) ----------
186
357
  if (step.type === 'loop') {
358
+ const isWhileMode = step.over === undefined;
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
+ }
375
+ const useFreshContext = step.fresh_context === true;
376
+ // fresh context:循环开始时 snapshot 当前 history
377
+ let baseHistorySnapshot = null;
378
+ if (useFreshContext) {
379
+ baseHistorySnapshot = ctx.history.slice();
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;
402
+ if (isWhileMode) {
403
+ // ===== WHILE 循环模式 =====
404
+ const asName = step.as ?? '_iter';
405
+ let iterCount = resumeIterStart;
406
+ let shouldBreak = false;
407
+ while (iterCount < maxIter) {
408
+ if (ctx.signal?.aborted) {
409
+ yield { type: 'interrupted' };
410
+ return 'halted';
411
+ }
412
+ // 注入迭代变量
413
+ vars.push();
414
+ vars.set(asName, iterCount);
415
+ vars.set(`${asName}_idx`, iterCount);
416
+ // fresh context:每轮重置 history 到循环入口快照
417
+ if (useFreshContext && baseHistorySnapshot) {
418
+ ctx.history.length = 0;
419
+ ctx.history.push(...baseHistorySnapshot);
420
+ }
421
+ // 迭代序号对用户有真实意义(第几轮重做),title 步也保留,只换人话名。
422
+ yield {
423
+ type: 'plugin_progress',
424
+ pluginId: WF_PLUGIN_ID,
425
+ current: iterCount + 1,
426
+ max: maxIter,
427
+ message: step.title
428
+ ? `${interpolate(step.title, vars)}·第 ${iterCount + 1}/${maxIter} 轮`
429
+ : `while-loop ${step.id} 迭代 ${iterCount + 1}/${maxIter}`,
430
+ };
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
+ };
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();
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
+ }
459
+ }
460
+ const whyEnd = shouldBreak ? 'break 退出' : `达到上限 ${maxIter}`;
461
+ await state.appendProgress(`✓ ${step.id} (while ×${iterCount}, ${whyEnd})`);
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
+ }
468
+ yield {
469
+ type: 'workflow_step_end',
470
+ id: step.id,
471
+ ok: true,
472
+ output: `while loop ${iterCount} iterations (${whyEnd})`,
473
+ };
474
+ continue;
475
+ }
476
+ // ===== FOR-EACH 循环模式(原有逻辑) =====
187
477
  let arr;
188
478
  try {
189
479
  arr = evalExpr(step.over ?? '[]', vars);
@@ -193,33 +483,50 @@ async function* runSteps(steps, vars, ctx, state) {
193
483
  await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
194
484
  yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
195
485
  yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
196
- return true;
486
+ return 'halted';
197
487
  }
198
488
  if (!Array.isArray(arr)) {
199
489
  const errMsg = `loop over 必须求值为数组,得到 ${typeof arr}`;
200
490
  await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
201
491
  yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
202
492
  yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
203
- return true;
493
+ return 'halted';
204
494
  }
205
495
  const asName = step.as ?? 'item';
206
496
  let iterCount = 0;
207
- for (let idx = 0; idx < arr.length; idx++) {
497
+ for (let idx = resumeIterStart; idx < arr.length; idx++) {
208
498
  if (ctx.signal?.aborted) {
209
499
  yield { type: 'interrupted' };
210
- return true;
500
+ return 'halted';
211
501
  }
212
502
  vars.push();
213
503
  vars.set(asName, arr[idx]);
214
504
  vars.set(`${asName}_idx`, idx);
215
- const childHalted = yield* runSteps(step.do ?? [], vars, ctx, state);
505
+ // fresh context 也支持 for-each 模式
506
+ if (useFreshContext && baseHistorySnapshot) {
507
+ ctx.history.length = 0;
508
+ ctx.history.push(...baseHistorySnapshot);
509
+ }
510
+ const innerStart = pendingInnerStart;
511
+ pendingInnerStart = 0;
512
+ const r = yield* runSteps(step.do ?? [], vars, ctx, state, innerCpFor(idx), innerStart);
216
513
  vars.pop();
217
- if (childHalted)
218
- return true;
219
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
+ }
220
524
  }
221
525
  await state.appendProgress(`✓ ${step.id} (loop ×${iterCount})`);
222
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
+ }
223
530
  yield {
224
531
  type: 'workflow_step_end',
225
532
  id: step.id,
@@ -228,258 +535,379 @@ async function* runSteps(steps, vars, ctx, state) {
228
535
  };
229
536
  continue;
230
537
  }
231
- // ---------- 控制流:pause ----------
538
+ // ---------- 控制流:pause(真 HITL 审批闸门) ----------
232
539
  //
233
- // 非交互模式(CLI -p / 测试)下无法真的暂停等待人工,直接当成 skipped。
234
- // 交互模式(TUI)需要 UI 层接住 step_skipped 把 reason 渲染出来 —— P0 已
235
- // 走通这条 UI 路径。step.prompt 用于把"暂停理由"传给上层呈现。
540
+ // 经 AskUserQuestion/askBridge 实现真停等:TUI 弹选择界面、用户拍板后继续;
541
+ // 非交互宿主(-p/gateway/测试)askBridge 自动选推荐项(继续执行)且 stderr
542
+ // 注记,绝不挂起——与 -p 单行 JSON 契约兼容。「等用户」骑在既有
543
+ // tool_call→tool_result 回路上(同 AskUserQuestion 工具),引擎零新通道。
236
544
  if (step.type === 'pause') {
237
545
  const msg = step.prompt
238
546
  ? interpolate(step.prompt, vars)
239
- : 'pause(非交互模式直接跳过)';
240
- await state.appendProgress(`- ${step.id} pause: ${msg}`);
241
- // P3:pause 在当前实现下**从不**真正等待人工(runner 是纯 AsyncGenerator,
242
- // 没有把用户输入回灌进来的通道)。把"静默放行"变"响亮放行"——发通用 warning
243
- // (-p 模式由 print.ts 落 stderr,权威通道)并镜像 plugin_progress(TUI 状态行可见),
244
- // 避免审批闸门被悄悄跳过。真·HITL pause 属 P4,本轮不做。
245
- const warnMsg = `⚠ pause "${step.id}" 在非交互模式被自动跳过,未真正等待人工确认——若这是审批闸门请勿依赖它。提示:${msg}`;
246
- yield { type: 'warning', message: warnMsg };
247
- yield {
248
- type: 'plugin_progress',
249
- pluginId: WF_PLUGIN_ID,
250
- current: 0,
251
- message: warnMsg,
252
- };
253
- yield {
254
- type: 'workflow_step_skipped',
255
- id: step.id,
256
- reason: `pause: ${msg}`,
257
- };
258
- yield {
259
- type: 'workflow_step_end',
260
- id: step.id,
261
- ok: true,
262
- output: 'paused (skipped in non-interactive mode)',
263
- };
264
- continue;
265
- }
266
- // ---------- 控制流:parallel ----------
267
- //
268
- // 每个 fork 独立 VarStack 副本 + 独立事件缓冲;Promise.all 并发执行。
269
- // 子 fork 的事件先收进 buffer,再按 fork 顺序一次性 yield,保 UI
270
- // 时间轴可读(并发 yield 会导致 UI 拿到乱序事件)。
271
- // 任一 fork halt → 整个 parallel halt。
272
- if (step.type === 'parallel') {
273
- const forks = step.fork ?? [];
274
- const results = await Promise.all(forks.map(async (forkStep) => {
275
- const forkVars = vars.fork();
276
- const events = [];
277
- let halted = false;
278
- try {
279
- halted = await collectStepsToBuffer([forkStep], forkVars, ctx, state, events);
280
- }
281
- catch (e) {
282
- halted = true;
283
- events.push({
284
- type: 'error',
285
- error: `fork ${forkStep.id} 抛异常: ${e.message}`,
286
- });
287
- }
288
- return {
289
- id: forkStep.id,
290
- events,
291
- vars: forkVars.snapshot(),
292
- halted,
293
- };
294
- }));
295
- // 顺序 yield 所有 fork 事件
296
- for (const r of results) {
297
- for (const ev of r.events)
298
- yield ev;
299
- }
300
- if (results.some((r) => r.halted)) {
301
- await state.appendProgress(`✗ ${step.id}: parallel: 至少一个 fork 失败`);
547
+ : '工作流暂停点:请确认是否继续执行后续步骤。';
548
+ await state.appendProgress(`- ${step.id} pause: 等待用户确认`);
549
+ // pause 会驻留状态行很久(等用户),title 步同样不带块内序号防误读。
550
+ if (step.title) {
302
551
  yield {
303
- type: 'workflow_step_end',
304
- id: step.id,
305
- ok: false,
306
- error: 'parallel: 至少一个 fork 失败',
552
+ type: 'plugin_progress',
553
+ pluginId: WF_PLUGIN_ID,
554
+ message: `⏸ ${interpolate(step.title, vars)}:等待用户确认…`,
307
555
  };
556
+ }
557
+ else {
308
558
  yield {
309
- type: 'error',
310
- 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}:等待用户确认…`,
311
564
  };
312
- return true;
313
565
  }
314
- // 聚合:raw.fork 是 [{ id, vars }, ...],capture 可走 fork.0.vars.xxx 这种点路径
315
- const aggResult = {
316
- raw: { fork: results.map((r) => ({ id: r.id, vars: r.vars })) },
317
- preview: `parallel: ${results.length} forks done`,
318
- };
319
- if (step.capture)
320
- bindCapture(step.capture, aggResult, vars);
321
- await state.appendProgress(`✓ ${step.id} (parallel ×${results.length})`);
322
- await state.writeVars(vars.snapshot());
323
- yield {
324
- type: 'workflow_step_end',
325
- id: step.id,
326
- ok: true,
327
- output: aggResult.preview,
328
- };
329
- continue;
330
- }
331
- // ---------- 控制流:vote ----------
332
- //
333
- // 对每个 input 数组项跑 N 个 voter(同 prompt + refuted: boolean),
334
- // refuted 票数 >= threshold 则视为驳回(item 不进 survived)。
335
- // 心理学定位:voter 倾向于 refuted=true(找漏洞),所以阈值算"驳回票数"。
336
- if (step.type === 'vote') {
337
- const voters_count = step.voters_count ?? 3;
338
- const threshold = step.threshold ?? Math.ceil(voters_count / 2);
339
- 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';
340
574
  try {
341
- 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
+ }
342
611
  }
343
- catch (e) {
344
- const errMsg = `vote.input 求值失败: ${e.message}`;
345
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
346
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
347
- yield {
348
- type: 'error',
349
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
350
- };
351
- return true;
612
+ catch {
613
+ askFailed = true; // 桥不可用 → 放行 + 响亮警告(保持旧"不挂起"行为)
352
614
  }
353
- if (!Array.isArray(inputArr)) {
354
- const errMsg = `vote.input 必须求值为数组,得到 ${typeof inputArr}`;
355
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
356
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
615
+ if (askFailed) {
357
616
  yield {
358
- type: 'error',
359
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
617
+ type: 'warning',
618
+ message: `⚠ pause "${step.id}" 审批闸门不可用(提问桥异常),已自动放行。提示:${msg}`,
360
619
  };
361
- return true;
362
620
  }
363
- const survived = [];
364
- for (const item of inputArr) {
365
- if (ctx.signal?.aborted) {
366
- yield { type: 'interrupted' };
367
- return true;
368
- }
369
- const verdicts = await Promise.all(Array.from({ length: voters_count }, () => runVoter(step.rules ?? '', item, vars, ctx)));
370
- const refutedCount = verdicts.filter((v) => v.refuted === true).length;
371
- if (refutedCount < threshold) {
372
- survived.push(item);
373
- }
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';
374
631
  }
375
- const voteResult = {
632
+ // 带补充要求的放行:自由输入并入贯穿目标(后续每步都看到,不止 capture 变量)
633
+ if (freeText)
634
+ await addGoalSupplement(ctx, state, freeText);
635
+ const pauseResult = {
376
636
  raw: {
377
- survived,
378
- total: inputArr.length,
379
- kept: survived.length,
637
+ result: freeText,
638
+ text: freeText,
639
+ approved: true,
640
+ free_text: freeText,
380
641
  },
381
- preview: `vote: ${survived.length}/${inputArr.length} survived (threshold=${threshold}/${voters_count})`,
642
+ preview: freeText
643
+ ? `审批通过:继续执行;补充要求:${truncate(freeText, 120)}`
644
+ : '审批通过:继续执行',
382
645
  };
383
646
  if (step.capture)
384
- bindCapture(step.capture, voteResult, vars);
385
- await state.appendProgress(`✓ ${step.id} (vote ${survived.length}/${inputArr.length})`);
647
+ bindCapture(step.capture, pauseResult, vars);
386
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: 已确认继续)`);
387
653
  yield {
388
654
  type: 'workflow_step_end',
389
655
  id: step.id,
390
656
  ok: true,
391
- output: voteResult.preview,
657
+ output: pauseResult.preview,
392
658
  };
393
659
  continue;
394
660
  }
395
- // ---------- 普通动作 step:tool / llm / skill / assert ----------
396
- try {
397
- const result = yield* dispatchActionStep(step, vars, ctx);
398
- if (step.capture)
399
- bindCapture(step.capture, result, vars);
400
- await state.appendProgress(`✓ ${step.id} (${kind})`);
401
- await state.writeVars(vars.snapshot());
661
+ // ---------- 控制流:break / continue(循环信号) ----------
662
+ //
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;
670
+ if (step.when) {
671
+ try {
672
+ shouldFire = Boolean(evalExpr(step.when, vars));
673
+ }
674
+ catch (e) {
675
+ // when 表达式错误 → 不触发,当 skipped 处理
676
+ shouldFire = false;
677
+ }
678
+ }
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 };
684
+ }
685
+ // 条件不满足 → 当 skip 处理,继续下一步
402
686
  yield {
403
- type: 'workflow_step_end',
687
+ type: 'workflow_step_skipped',
404
688
  id: step.id,
405
- ok: true,
406
- output: result.preview,
689
+ reason: `${step.type} 条件不满足`,
407
690
  };
691
+ continue;
408
692
  }
409
- catch (e) {
410
- const errMsg = e.message ?? String(e);
411
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
412
- // onError:'recover' —— 不立刻 halt,交给有界恢复 agent 随机应变把这一步救回来。
413
- // 「确定性工作流 × TAOR」的结合点:happy path 走刚性 DAG,撞上预料之外的就地
414
- // 降级为 agent 自适应;救回则继续 happy path,救不回才 honest halt。
415
- if (step.onError === 'recover') {
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) {
416
703
  yield {
417
704
  type: 'warning',
418
- message: `step "${step.id}" 失败:${errMsg};尝试自适应恢复…`,
705
+ message: `step "${step.id}": 模板变量未定义,已按空串插值:${[...new Set(missing)].map((m) => `\${${m}}`).join('、')}`,
419
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
+ }
713
+ yield {
714
+ type: 'workflow_step_end',
715
+ id: step.id,
716
+ ok: true,
717
+ output: truncate(text, 200),
718
+ };
719
+ continue;
720
+ }
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
+ }
420
746
  yield {
421
- type: 'plugin_progress',
422
- pluginId: WF_PLUGIN_ID,
423
- current: i + 1,
424
- max: steps.length,
425
- message: `↻ 恢复 ${step.id}…`,
747
+ type: 'workflow_step_end',
748
+ id: step.id,
749
+ ok: true,
750
+ output: result.preview,
426
751
  };
427
- const rec = yield* runRecovery(step, vars, ctx, errMsg);
428
- if (ctx.signal?.aborted) {
429
- yield { type: 'interrupted' };
430
- 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; // 重跑同一步
431
790
  }
432
- // 验证闸门:只有恢复 agent 真正交付(rec.ok)才绑 capture 走下去;
433
- // 显式放弃 / 产出不可信(rec.ok=false)则带原因 honest halt,绝不盲绑脏值。
434
- if (rec.ok) {
435
- if (step.capture)
436
- bindCapture(step.capture, rec.value, vars);
437
- await state.appendProgress(`⚠→✓ ${step.id} recovered`);
438
- 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);
439
833
  yield {
440
834
  type: 'workflow_step_end',
441
835
  id: step.id,
442
- ok: true,
443
- output: rec.value.preview,
836
+ ok: false,
837
+ error: combined,
444
838
  };
445
- continue;
839
+ yield {
840
+ type: 'error',
841
+ error: `Workflow halted at step "${step.id}": ${combined}`,
842
+ };
843
+ return 'halted';
844
+ }
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; // 跳过本步 → 进入下一步
446
869
  }
447
- const combined = `${errMsg}(自适应恢复未成功:${rec.reason})`;
870
+ const finalErrMsg = isMaxTurnsCutoff(errMsg)
871
+ ? `${errMsg};重试额度已用尽,请拆分该步骤或提高 maxTurns。`
872
+ : errMsg;
873
+ if (cp)
874
+ await state.recordFailure(step.id, finalErrMsg);
448
875
  yield {
449
876
  type: 'workflow_step_end',
450
877
  id: step.id,
451
878
  ok: false,
452
- error: combined,
879
+ error: finalErrMsg,
453
880
  };
454
881
  yield {
455
882
  type: 'error',
456
- error: `Workflow halted at step "${step.id}": ${combined}`,
883
+ error: `Workflow halted at step "${step.id}": ${finalErrMsg}`,
457
884
  };
458
- return true;
885
+ return 'halted';
459
886
  }
460
- if (step.onError === 'continue') {
461
- yield {
462
- type: 'workflow_step_end',
463
- id: step.id,
464
- ok: false,
465
- error: errMsg,
466
- };
467
- continue;
468
- }
469
- yield {
470
- type: 'workflow_step_end',
471
- id: step.id,
472
- ok: false,
473
- error: errMsg,
474
- };
475
- yield {
476
- type: 'error',
477
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
478
- };
479
- return true;
480
887
  }
481
888
  }
482
- 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];
483
911
  }
484
912
  /**
485
913
  * 运行一个 workflow。
@@ -499,26 +927,152 @@ export async function* runWorkflow(def, inputs, ctx) {
499
927
  // onError:'recover' 用:把工作流总目标(description)透传给恢复 agent,
500
928
  // 让它知道「这条工作流到底想干什么」。已有 goal 则不覆盖。
501
929
  const runCtx = { ...ctx, goal: ctx.goal ?? def.description };
502
- const state = new WorkflowState(getWorkingDir());
503
- await state.init(def.name, inputs);
930
+ // 通用目标锚定:从 inputs 中提取用户的原始目标,
931
+ // 注入 RunContext.goalAnchor,让 agent/llm/skill 步自动前置到每步 prompt。
932
+ // 提取策略:优先 "goal" 命名的输入 → 其次第一个 required string 输入。
933
+ runCtx.goalAnchor = buildGoalAnchor(def.inputs ?? [], inputs);
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
+ }));
504
954
  yield {
505
955
  type: 'workflow_start',
506
956
  name: def.name,
507
957
  totalSteps: def.steps.length,
508
958
  };
509
- // 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)。
510
1053
  yield {
511
1054
  type: 'plugin_progress',
512
1055
  pluginId: WF_PLUGIN_ID,
513
- current: 0,
1056
+ current: startFrom,
514
1057
  max: def.steps.length,
515
1058
  message: `▶ workflow ${def.name}`,
516
1059
  };
517
1060
  let succeeded = false;
518
1061
  try {
519
- const halted = yield* runSteps(def.steps, vars, runCtx, state);
520
- 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')
521
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
+ }
522
1076
  // 完成态也给一条 plugin_progress(放在 workflow_done 之前,保证
523
1077
  // workflow_done 仍是事件流最后一条——e2e 测试依赖 events[last]===workflow_done)。
524
1078
  yield {
@@ -528,56 +1082,17 @@ export async function* runWorkflow(def, inputs, ctx) {
528
1082
  max: def.steps.length,
529
1083
  message: `✓ workflow ${def.name} 完成`,
530
1084
  };
1085
+ // v0.9 重构:不再需要 assistant_message 摘要注入。
1086
+ // agent/llm/skill 步已通过 runQuery(TAOR) 直接写入共享 ctx.history,
1087
+ // 每步的完整对话上下文(user→assistant→tool→...)都自然积累在 history 中。
1088
+ // 后续对话和用户调整都能看到完整的工作流执行过程。
1089
+ await state.markDone();
531
1090
  yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
532
1091
  succeeded = true;
533
- // ---- 层1:写入摘要到对话历史(让后续对话记住这次 workflow 做了什么)----
534
- const snapshot = vars.snapshot();
535
- const summaryText = typeof snapshot.summary_text === 'string'
536
- ? snapshot.summary_text
537
- : '';
538
- // 收集关键产出物(从 vars 中提取文件路径)
539
- const artifacts = [];
540
- if (typeof snapshot.plan_text === 'string')
541
- artifacts.push('./PLAN.md');
542
- // 检查是否有执行结果(execute_plan 步的产出)
543
- if (typeof snapshot.execution_result === 'string') {
544
- // 尝试从执行结果中提取文件路径(常见模式:产出物路径)
545
- const fileMatches = snapshot.execution_result.match(/`[^`]*\.(pptx|docx|pdf|xlsx|md|ts|js|py)[^`]*`/g);
546
- if (fileMatches)
547
- artifacts.push(...fileMatches.map((f) => f.replace(/`/g, '')));
548
- }
549
- const summaryLines = [
550
- `**工作流「${def.name}」执行完成**`,
551
- ``,
552
- `- 步骤: ${def.steps.length}/${def.steps.length} 全部完成`,
553
- ...(artifacts.length > 0 ? [`- 产出物: ${artifacts.join(', ')}`] : []),
554
- ...(summaryText ? [`- 摘要: ${summaryText}`] : []),
555
- ];
556
- yield {
557
- type: 'assistant_message',
558
- message: {
559
- role: 'assistant',
560
- content: summaryLines.join('\n'),
561
- },
562
- };
563
1092
  }
564
1093
  finally {
565
- // 成功时归档(保留 vars.json/progress.md 供后续回溯);失败/中断时保留原状供排查
1094
+ // 成功时归档(保留 vars.json/progress.md/checkpoints 供后续回溯);失败/中断时保留原状供排查与 resume
566
1095
  if (succeeded)
567
1096
  await state.archive();
568
1097
  }
569
1098
  }
570
- /**
571
- * parallel fork 用:把 runSteps 的 yield 流收集到 buffer 数组,
572
- * 让外层 Promise.all 拿到完整事件后再顺序 yield。
573
- *
574
- * 注:for-await 无法读取 generator 的 return value(JS 限制),
575
- * 所以"是否 halted"靠"buffer 里有 error event"近似判断 —— 这符合
576
- * runSteps 的语义(halt 一定会 yield error / interrupted)。
577
- */
578
- async function collectStepsToBuffer(steps, vars, ctx, state, buffer) {
579
- for await (const ev of runSteps(steps, vars, ctx, state)) {
580
- buffer.push(ev);
581
- }
582
- return buffer.some((ev) => ev.type === 'error' || ev.type === 'interrupted');
583
- }