minimal-agent 0.10.0 → 0.12.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 (573) hide show
  1. package/README.md +11 -3
  2. package/package.json +52 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
  4. package/plugins/computer_use/README.md +94 -0
  5. package/plugins/computer_use/app/_template.md +142 -0
  6. package/plugins/computer_use/app/chrome.md +165 -0
  7. package/plugins/computer_use/app/finder.md +76 -0
  8. package/plugins/computer_use/app/macos-system-settings.md +81 -0
  9. package/plugins/computer_use/app/microsoft-excel.md +80 -0
  10. package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
  11. package/plugins/computer_use/app/microsoft-word.md +79 -0
  12. package/plugins/computer_use/app/wechat.md +413 -0
  13. package/plugins/computer_use/app/weread.md +319 -0
  14. package/plugins/computer_use/commands/computeruse.md +25 -0
  15. package/plugins/computer_use/commands/cu.md +9 -0
  16. package/plugins/computer_use/plugin.js +26 -0
  17. package/plugins/computer_use/plugin.ts +30 -0
  18. package/plugins/computer_use/src/capture.js +90 -0
  19. package/plugins/computer_use/src/capture.ts +104 -0
  20. package/plugins/computer_use/src/commands.js +91 -0
  21. package/plugins/computer_use/src/commands.ts +103 -0
  22. package/plugins/computer_use/src/daemon.js +134 -0
  23. package/plugins/computer_use/src/daemon.ts +161 -0
  24. package/plugins/computer_use/src/manuals.js +135 -0
  25. package/plugins/computer_use/src/manuals.ts +159 -0
  26. package/plugins/computer_use/src/verify.js +168 -0
  27. package/plugins/computer_use/src/verify.ts +219 -0
  28. package/plugins/computer_use/test/commands.test.ts +73 -0
  29. package/plugins/cron-runner/plugin.ts +38 -0
  30. package/plugins/cron-runner/src/commands.ts +373 -0
  31. package/plugins/cron-runner/src/cronTime.ts +155 -0
  32. package/plugins/cron-runner/src/lock.ts +79 -0
  33. package/plugins/cron-runner/src/runs.ts +99 -0
  34. package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
  35. package/plugins/cron-runner/src/scheduler.ts +376 -0
  36. package/plugins/cron-runner/src/schema.ts +139 -0
  37. package/plugins/cron-runner/src/service.ts +193 -0
  38. package/plugins/cron-runner/src/store.ts +333 -0
  39. package/plugins/cron-runner/src/types.ts +141 -0
  40. package/plugins/cron-runner/test/cronTime.test.ts +41 -0
  41. package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
  42. package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
  43. package/plugins/cron-runner/test/service.test.ts +76 -0
  44. package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
  45. package/plugins/workflow-runner/plugin.js +56 -2
  46. package/plugins/workflow-runner/plugin.ts +113 -0
  47. package/plugins/workflow-runner/src/agentCommon.js +43 -4
  48. package/plugins/workflow-runner/src/agentCommon.ts +435 -0
  49. package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
  50. package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
  51. package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
  52. package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
  53. package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
  54. package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
  55. package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
  56. package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
  57. package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
  58. package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
  59. package/plugins/workflow-runner/src/dslManifest.ts +127 -0
  60. package/plugins/workflow-runner/src/expressions.ts +622 -0
  61. package/plugins/workflow-runner/src/index.js +13 -4
  62. package/plugins/workflow-runner/src/index.ts +338 -0
  63. package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
  64. package/plugins/workflow-runner/src/loader.js +23 -0
  65. package/plugins/workflow-runner/src/loader.ts +721 -0
  66. package/plugins/workflow-runner/src/multimodal.js +2 -1
  67. package/plugins/workflow-runner/src/multimodal.ts +64 -0
  68. package/plugins/workflow-runner/src/recovery.ts +149 -0
  69. package/plugins/workflow-runner/src/runner.js +75 -659
  70. package/plugins/workflow-runner/src/runner.ts +604 -0
  71. package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
  72. package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
  73. package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
  74. package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
  75. package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
  76. package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
  77. package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
  78. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  79. package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
  80. package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
  81. package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
  82. package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
  83. package/plugins/workflow-runner/src/stepPipeline.js +277 -0
  84. package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
  85. package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
  86. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  87. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  88. package/plugins/workflow-runner/src/types.js +37 -1
  89. package/plugins/workflow-runner/src/types.ts +602 -0
  90. package/plugins/workflow-runner/src/workflowState.ts +733 -0
  91. package/plugins/workflow-runner/test/agent.test.ts +587 -0
  92. package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
  93. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
  94. package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
  95. package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
  96. package/plugins/workflow-runner/test/context-files.test.ts +320 -0
  97. package/plugins/workflow-runner/test/e2e.test.ts +370 -0
  98. package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
  99. package/plugins/workflow-runner/test/expressions.test.ts +397 -0
  100. package/plugins/workflow-runner/test/foundation.test.ts +126 -0
  101. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  102. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
  103. package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
  104. package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
  105. package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
  106. package/plugins/workflow-runner/test/loader.test.ts +617 -0
  107. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
  108. package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
  109. package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
  110. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
  111. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
  112. package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
  113. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
  114. package/plugins/workflow-runner/test/recovery.test.ts +265 -0
  115. package/plugins/workflow-runner/test/runner.test.ts +911 -0
  116. package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
  117. package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
  118. package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
  119. package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
  120. package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
  121. package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
  122. package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
  123. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
  124. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
  125. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  126. package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
  127. package/plugins/workflow-runner/test/word.load.test.ts +208 -0
  128. package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
  129. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
  130. package/skills/browser-automation/SKILL.md +9 -19
  131. package/skills/web-crawler/SKILL.md +9 -11
  132. package/src/askBridge.ts +249 -0
  133. package/src/bootstrap/cwdArg.js +10 -3
  134. package/src/bootstrap/cwdArg.ts +31 -0
  135. package/src/bootstrap/workingDir.ts +48 -0
  136. package/src/cli/args.js +70 -7
  137. package/src/cli/args.ts +146 -0
  138. package/src/cli/computerUse.js +185 -0
  139. package/src/cli/computerUse.ts +221 -0
  140. package/src/cli/configMenu.js +274 -202
  141. package/src/cli/configMenu.tsx +521 -0
  142. package/src/cli/memoryAccess.js +45 -0
  143. package/src/cli/memoryAccess.ts +53 -0
  144. package/src/cli/print.js +119 -5
  145. package/src/cli/print.ts +597 -0
  146. package/src/cli/quickModelSetup.js +60 -0
  147. package/src/cli/quickModelSetup.tsx +96 -0
  148. package/src/cli/streamJson.js +80 -4
  149. package/src/cli/streamJson.ts +433 -0
  150. package/src/cli/wizards/advisorWizard.js +1 -0
  151. package/src/cli/wizards/advisorWizard.tsx +195 -0
  152. package/src/cli/wizards/agentRuntimeWizard.js +84 -0
  153. package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
  154. package/src/cli/wizards/browserWizard.js +55 -0
  155. package/src/cli/wizards/browserWizard.tsx +77 -0
  156. package/src/cli/wizards/contextAdvisorWizard.js +84 -0
  157. package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
  158. package/src/cli/wizards/fieldForm.js +7 -4
  159. package/src/cli/wizards/fieldForm.tsx +250 -0
  160. package/src/cli/wizards/imageWizard.js +4 -5
  161. package/src/cli/wizards/imageWizard.tsx +96 -0
  162. package/src/cli/wizards/openRouterWizard.tsx +48 -0
  163. package/src/cli/wizards/providerPresets.js +1 -8
  164. package/src/cli/wizards/providerPresets.ts +106 -0
  165. package/src/cli/wizards/providerWizard.js +152 -205
  166. package/src/cli/wizards/providerWizard.tsx +266 -0
  167. package/src/cli/wizards/searchWizard.tsx +47 -0
  168. package/src/cli/wizards/ttsWizard.js +4 -3
  169. package/src/cli/wizards/ttsWizard.tsx +61 -0
  170. package/src/config/configDefaults.js +25 -0
  171. package/src/config/configDefaults.ts +33 -0
  172. package/src/config/configDiagnostics.js +117 -0
  173. package/src/config/configDiagnostics.ts +166 -0
  174. package/src/config/configFile.js +52 -32
  175. package/src/config/configFile.ts +633 -0
  176. package/src/config/configManagement.js +126 -0
  177. package/src/config/configManagement.ts +159 -0
  178. package/src/config/configService.js +66 -11
  179. package/src/config/configService.ts +554 -0
  180. package/src/config/numericLimits.js +61 -0
  181. package/src/config/numericLimits.ts +80 -0
  182. package/src/config.ts +109 -0
  183. package/src/context/agentHome.ts +11 -0
  184. package/src/context/archive.ts +144 -0
  185. package/src/context/budgetPlan.js +54 -0
  186. package/src/context/budgetPlan.ts +88 -0
  187. package/src/context/compact.js +179 -106
  188. package/src/context/compact.ts +584 -0
  189. package/src/context/contextBundle.js +119 -0
  190. package/src/context/contextBundle.ts +176 -0
  191. package/src/context/contextLock.ts +84 -0
  192. package/src/context/microCompactLite.ts +218 -0
  193. package/src/context/persistContext.ts +177 -0
  194. package/src/context/reactiveCompact.js +19 -4
  195. package/src/context/reactiveCompact.ts +240 -0
  196. package/src/context/recentDirs.ts +42 -0
  197. package/src/context/sessionPath.ts +206 -0
  198. package/src/context/sessionRegistry.js +8 -2
  199. package/src/context/sessionRegistry.ts +414 -0
  200. package/src/context/snipCompact.ts +163 -0
  201. package/src/context/tokenCounter.ts +106 -0
  202. package/src/context/workspaceIndex.js +2 -2
  203. package/src/context/workspaceIndex.ts +751 -0
  204. package/src/entry.compiled.tsx +20 -0
  205. package/src/harness/claimValidation.js +9 -0
  206. package/src/harness/claimValidation.ts +22 -0
  207. package/src/harness/evidenceLedger.js +92 -0
  208. package/src/harness/evidenceLedger.ts +96 -0
  209. package/src/harness/stallGuard.js +60 -0
  210. package/src/harness/stallGuard.ts +81 -0
  211. package/src/humanIntervention.js +129 -0
  212. package/src/humanIntervention.ts +216 -0
  213. package/src/infra/verification.ts +295 -0
  214. package/src/irreversible/rules.js +216 -0
  215. package/src/irreversible/rules.ts +261 -0
  216. package/src/llm/client.js +40 -3
  217. package/src/llm/client.ts +621 -0
  218. package/src/llm/promptCache.js +27 -0
  219. package/src/llm/promptCache.ts +41 -0
  220. package/src/llm/providers/anthropicFormat.js +41 -5
  221. package/src/llm/providers/anthropicFormat.ts +290 -0
  222. package/src/llm/providers/openaiFormat.js +186 -8
  223. package/src/llm/providers/openaiFormat.ts +428 -0
  224. package/src/loop.js +614 -63
  225. package/src/loop.ts +1454 -0
  226. package/src/main.js +67 -10
  227. package/src/main.tsx +344 -0
  228. package/src/memory/codec.js +204 -0
  229. package/src/memory/codec.ts +249 -0
  230. package/src/memory/constants.js +34 -0
  231. package/src/memory/constants.ts +50 -0
  232. package/src/memory/episodicMemory.js +684 -0
  233. package/src/memory/episodicMemory.ts +815 -0
  234. package/src/memory/fileStore.js +148 -0
  235. package/src/memory/fileStore.ts +184 -0
  236. package/src/memory/memoryService.js +570 -0
  237. package/src/memory/memoryService.ts +859 -0
  238. package/src/memory/migration.js +128 -0
  239. package/src/memory/migration.ts +160 -0
  240. package/src/memory/repository.js +789 -0
  241. package/src/memory/repository.ts +893 -0
  242. package/src/memory/retrieval.js +108 -0
  243. package/src/memory/retrieval.ts +145 -0
  244. package/src/memory/searchIndex.js +107 -0
  245. package/src/memory/searchIndex.ts +153 -0
  246. package/src/memory/topicView.js +82 -0
  247. package/src/memory/topicView.ts +95 -0
  248. package/src/memory/turnJournal.js +95 -0
  249. package/src/memory/turnJournal.ts +120 -0
  250. package/src/memory/types.js +2 -0
  251. package/src/memory/types.ts +186 -0
  252. package/src/messageQueue.ts +123 -0
  253. package/src/multimodal.js +128 -46
  254. package/src/multimodal.ts +349 -0
  255. package/src/plugin-sdk.js +21 -1
  256. package/src/plugin-sdk.ts +134 -0
  257. package/src/plugins/builtinPluginIds.js +47 -0
  258. package/src/plugins/builtinPluginIds.ts +51 -0
  259. package/src/plugins/builtinRegistry.ts +43 -0
  260. package/src/plugins/commandRouter.js +40 -26
  261. package/src/plugins/commandRouter.ts +261 -0
  262. package/src/plugins/hookEngine.ts +341 -0
  263. package/src/plugins/pluginApi.ts +133 -0
  264. package/src/plugins/pluginLifecycle.ts +34 -0
  265. package/src/plugins/pluginLoader.ts +147 -0
  266. package/src/plugins/pluginManifest.js +127 -0
  267. package/src/plugins/pluginManifest.ts +152 -0
  268. package/src/plugins/pluginRunner.js +47 -2
  269. package/src/plugins/pluginRunner.ts +207 -0
  270. package/src/plugins/transcript.ts +208 -0
  271. package/src/prompts/lint.js +169 -0
  272. package/src/prompts/lint.ts +220 -0
  273. package/src/prompts/pluginList.js +32 -16
  274. package/src/prompts/pluginList.ts +86 -0
  275. package/src/prompts/policyRegistry.js +318 -0
  276. package/src/prompts/policyRegistry.ts +358 -0
  277. package/src/prompts/projectInstructions.js +24 -0
  278. package/src/prompts/projectInstructions.ts +81 -0
  279. package/src/prompts/promptAssembly.js +311 -202
  280. package/src/prompts/promptAssembly.ts +510 -0
  281. package/src/prompts/skillList.js +106 -45
  282. package/src/prompts/skillList.ts +198 -0
  283. package/src/prompts/system.js +31 -6
  284. package/src/prompts/system.ts +81 -0
  285. package/src/runtime/agentRuntimeContext.js +53 -0
  286. package/src/runtime/agentRuntimeContext.ts +112 -0
  287. package/src/session/runAgentTurn.js +71 -12
  288. package/src/session/runAgentTurn.ts +181 -0
  289. package/src/session/runTurn.js +29 -0
  290. package/src/session/runTurn.ts +160 -0
  291. package/src/session/runtimeStore.js +149 -0
  292. package/src/session/runtimeStore.ts +197 -0
  293. package/src/session/sessionState.js +79 -3
  294. package/src/session/sessionState.ts +134 -0
  295. package/src/toolGate.ts +50 -0
  296. package/src/tools/advisor/advisor.js +76 -15
  297. package/src/tools/advisor/advisor.ts +512 -0
  298. package/src/tools/agent/agent.js +245 -29
  299. package/src/tools/agent/agent.ts +837 -0
  300. package/src/tools/agent/agentTaskService.js +60 -0
  301. package/src/tools/agent/agentTaskService.ts +113 -0
  302. package/src/tools/agent/agentTypes.js +20 -8
  303. package/src/tools/agent/agentTypes.ts +94 -0
  304. package/src/tools/agent/coordinator.ts +111 -0
  305. package/src/tools/agent/worktree.ts +296 -0
  306. package/src/tools/ask/ask.js +17 -0
  307. package/src/tools/ask/ask.ts +122 -0
  308. package/src/tools/bash/attachImage.js +223 -0
  309. package/src/tools/bash/attachImage.ts +281 -0
  310. package/src/tools/bash/bash.js +132 -14
  311. package/src/tools/bash/bash.ts +642 -0
  312. package/src/tools/bash/semantics.ts +103 -0
  313. package/src/tools/bash/warnings.js +17 -11
  314. package/src/tools/bash/warnings.ts +117 -0
  315. package/src/tools/capabilities.js +42 -0
  316. package/src/tools/capabilities.ts +69 -0
  317. package/src/tools/computeruse/accessibilityPermission.js +261 -0
  318. package/src/tools/computeruse/accessibilityPermission.ts +294 -0
  319. package/src/tools/computeruse/axSearch.js +96 -0
  320. package/src/tools/computeruse/axSearch.ts +120 -0
  321. package/src/tools/computeruse/axSnap.js +272 -0
  322. package/src/tools/computeruse/axSnap.ts +349 -0
  323. package/src/tools/computeruse/capabilityLabels.js +95 -0
  324. package/src/tools/computeruse/capabilityLabels.ts +115 -0
  325. package/src/tools/computeruse/computerUseService.js +2235 -0
  326. package/src/tools/computeruse/computerUseService.ts +2686 -0
  327. package/src/tools/computeruse/computeruse.js +2742 -0
  328. package/src/tools/computeruse/computeruse.ts +3265 -0
  329. package/src/tools/computeruse/diagnosticTrace.js +154 -0
  330. package/src/tools/computeruse/diagnosticTrace.ts +195 -0
  331. package/src/tools/computeruse/diagnostics.js +369 -0
  332. package/src/tools/computeruse/diagnostics.ts +453 -0
  333. package/src/tools/computeruse/grants.js +53 -0
  334. package/src/tools/computeruse/grants.ts +70 -0
  335. package/src/tools/computeruse/humanBoundary.js +159 -0
  336. package/src/tools/computeruse/humanBoundary.ts +199 -0
  337. package/src/tools/computeruse/intervention.js +77 -0
  338. package/src/tools/computeruse/intervention.ts +110 -0
  339. package/src/tools/computeruse/manualSchema.js +709 -0
  340. package/src/tools/computeruse/manualSchema.ts +916 -0
  341. package/src/tools/computeruse/observationImages.js +705 -0
  342. package/src/tools/computeruse/observationImages.ts +889 -0
  343. package/src/tools/computeruse/protocol.js +60 -0
  344. package/src/tools/computeruse/protocol.ts +515 -0
  345. package/src/tools/computeruse/telemetryLog.js +57 -0
  346. package/src/tools/computeruse/telemetryLog.ts +67 -0
  347. package/src/tools/computeruse/vision/appGuide.js +325 -0
  348. package/src/tools/computeruse/vision/appGuide.ts +482 -0
  349. package/src/tools/computeruse/vision/focus.js +291 -0
  350. package/src/tools/computeruse/vision/focus.ts +382 -0
  351. package/src/tools/computeruse/vision/frames.js +248 -0
  352. package/src/tools/computeruse/vision/frames.ts +329 -0
  353. package/src/tools/computeruse/vision/geometry.js +131 -0
  354. package/src/tools/computeruse/vision/geometry.ts +163 -0
  355. package/src/tools/computeruse/vision/imaging.js +201 -0
  356. package/src/tools/computeruse/vision/imaging.ts +281 -0
  357. package/src/tools/computeruse/vision/png.js +274 -0
  358. package/src/tools/computeruse/vision/png.ts +317 -0
  359. package/src/tools/computeruse/vision/raster.js +320 -0
  360. package/src/tools/computeruse/vision/raster.ts +408 -0
  361. package/src/tools/computeruse/vision/settle.js +131 -0
  362. package/src/tools/computeruse/vision/settle.ts +220 -0
  363. package/src/tools/computeruse/vision/telemetry.js +196 -0
  364. package/src/tools/computeruse/vision/telemetry.ts +241 -0
  365. package/src/tools/computeruse/visionOcr.js +115 -0
  366. package/src/tools/computeruse/visionOcr.ts +128 -0
  367. package/src/tools/computeruse/waitFor.js +237 -0
  368. package/src/tools/computeruse/waitFor.ts +291 -0
  369. package/src/tools/download/download.js +10 -6
  370. package/src/tools/download/download.ts +187 -0
  371. package/src/tools/edit/edit.js +30 -18
  372. package/src/tools/edit/edit.ts +349 -0
  373. package/src/tools/edit/multi-edit.js +25 -14
  374. package/src/tools/edit/multi-edit.ts +223 -0
  375. package/src/tools/effectiveToolSet.js +73 -0
  376. package/src/tools/effectiveToolSet.ts +91 -0
  377. package/src/tools/genimage/genimage.ts +257 -0
  378. package/src/tools/genspeech/genspeech.ts +83 -0
  379. package/src/tools/glob/glob.js +176 -29
  380. package/src/tools/glob/glob.ts +283 -0
  381. package/src/tools/grep/grep.js +61 -19
  382. package/src/tools/grep/grep.ts +239 -0
  383. package/src/tools/grep/rgPath.ts +206 -0
  384. package/src/tools/index.js +99 -5
  385. package/src/tools/index.ts +293 -0
  386. package/src/tools/lsp/config.js +113 -0
  387. package/src/tools/lsp/config.ts +113 -0
  388. package/src/tools/lsp/lsp.js +256 -0
  389. package/src/tools/lsp/lsp.ts +274 -0
  390. package/src/tools/lsp/service.js +303 -0
  391. package/src/tools/lsp/service.ts +296 -0
  392. package/src/tools/memory/memory.js +520 -0
  393. package/src/tools/memory/memory.ts +625 -0
  394. package/src/tools/memory/policy.js +53 -0
  395. package/src/tools/memory/policy.ts +72 -0
  396. package/src/tools/notebook/notebookEdit.js +114 -0
  397. package/src/tools/notebook/notebookEdit.ts +126 -0
  398. package/src/tools/powershell/powershell.js +210 -0
  399. package/src/tools/powershell/powershell.ts +239 -0
  400. package/src/tools/presentresult/presentresult.js +145 -0
  401. package/src/tools/presentresult/presentresult.ts +156 -0
  402. package/src/tools/read/read.js +371 -163
  403. package/src/tools/read/read.ts +558 -0
  404. package/src/tools/shared/diffSummary.js +44 -0
  405. package/src/tools/shared/diffSummary.ts +63 -0
  406. package/src/tools/shared/fileActivity.js +51 -0
  407. package/src/tools/shared/fileActivity.ts +66 -0
  408. package/src/tools/shared/fileMutationBus.js +13 -0
  409. package/src/tools/shared/fileMutationBus.ts +14 -0
  410. package/src/tools/shared/fileState.js +73 -65
  411. package/src/tools/shared/fileState.ts +145 -0
  412. package/src/tools/shared/fileUtils.js +81 -4
  413. package/src/tools/shared/fileUtils.ts +504 -0
  414. package/src/tools/shared/killTree.ts +50 -0
  415. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  416. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  417. package/src/tools/shared/schemas.ts +20 -0
  418. package/src/tools/shared/toolOutputStore.js +97 -0
  419. package/src/tools/shared/toolOutputStore.ts +110 -0
  420. package/src/tools/skill/skill.js +86 -0
  421. package/src/tools/skill/skill.ts +94 -0
  422. package/src/tools/snip/snip.js +67 -0
  423. package/src/tools/snip/snip.ts +80 -0
  424. package/src/tools/tasks/backgroundTaskService.js +275 -0
  425. package/src/tools/tasks/backgroundTaskService.ts +369 -0
  426. package/src/tools/tasks/processEvidence.js +24 -0
  427. package/src/tools/tasks/processEvidence.ts +34 -0
  428. package/src/tools/tasks/taskManagement.js +163 -0
  429. package/src/tools/tasks/taskManagement.ts +180 -0
  430. package/src/tools/tasks/taskOutput.js +113 -0
  431. package/src/tools/tasks/taskOutput.ts +145 -0
  432. package/src/tools/tasks/taskStop.js +74 -0
  433. package/src/tools/tasks/taskStop.ts +99 -0
  434. package/src/tools/tasks/taskStore.js +46 -0
  435. package/src/tools/tasks/taskStore.ts +77 -0
  436. package/src/tools/terminal/terminalCapture.js +167 -0
  437. package/src/tools/terminal/terminalCapture.ts +179 -0
  438. package/src/tools/trash/trash.js +233 -0
  439. package/src/tools/trash/trash.ts +283 -0
  440. package/src/tools/types.js +1 -1
  441. package/src/tools/types.ts +24 -0
  442. package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
  443. package/src/tools/webbrowser/browser.js +214 -11
  444. package/src/tools/webbrowser/browser.ts +293 -0
  445. package/src/tools/webbrowser/browserControl.js +153 -48
  446. package/src/tools/webbrowser/browserControl.ts +262 -0
  447. package/src/tools/webbrowser/browserDaemon.js +147 -39
  448. package/src/tools/webbrowser/browserDaemon.ts +270 -0
  449. package/src/tools/webbrowser/browserOutcome.js +273 -0
  450. package/src/tools/webbrowser/browserOutcome.ts +306 -0
  451. package/src/tools/webbrowser/browserRunRegistry.js +150 -0
  452. package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
  453. package/src/tools/webbrowser/browserService.js +20 -6
  454. package/src/tools/webbrowser/browserService.ts +56 -0
  455. package/src/tools/webbrowser/browserStream.js +30 -1
  456. package/src/tools/webbrowser/browserStream.ts +216 -0
  457. package/src/tools/webbrowser/browserTabs.js +143 -0
  458. package/src/tools/webbrowser/browserTabs.ts +210 -0
  459. package/src/tools/webbrowser/webbrowser.js +831 -150
  460. package/src/tools/webbrowser/webbrowser.ts +1537 -0
  461. package/src/tools/webfetch/preapproved.ts +274 -0
  462. package/src/tools/webfetch/webfetch.js +24 -23
  463. package/src/tools/webfetch/webfetch.ts +383 -0
  464. package/src/tools/websearch/websearch.js +12 -11
  465. package/src/tools/websearch/websearch.ts +292 -0
  466. package/src/tools/worktree/sessionWorktree.js +128 -0
  467. package/src/tools/worktree/sessionWorktree.ts +158 -0
  468. package/src/tools/write/write.js +27 -44
  469. package/src/tools/write/write.ts +136 -0
  470. package/src/types.ts +960 -0
  471. package/src/ui/App.js +216 -12
  472. package/src/ui/App.tsx +879 -0
  473. package/src/ui/AskQuestion.tsx +300 -0
  474. package/src/ui/InputBox.js +77 -1
  475. package/src/ui/InputBox.tsx +567 -0
  476. package/src/ui/LiveArea.tsx +344 -0
  477. package/src/ui/MessageList.js +27 -6
  478. package/src/ui/MessageList.tsx +559 -0
  479. package/src/ui/ModelPicker.tsx +107 -0
  480. package/src/ui/QueuedMessagesBanner.tsx +51 -0
  481. package/src/ui/Root.js +6 -8
  482. package/src/ui/Root.tsx +100 -0
  483. package/src/ui/SessionPicker.tsx +169 -0
  484. package/src/ui/StatusLine.js +22 -1
  485. package/src/ui/StatusLine.tsx +145 -0
  486. package/src/ui/WorkspacePicker.tsx +84 -0
  487. package/src/ui/advisorCommand.js +54 -0
  488. package/src/ui/advisorCommand.ts +63 -0
  489. package/src/ui/computerCommand.js +19 -0
  490. package/src/ui/computerCommand.ts +35 -0
  491. package/src/ui/figures.ts +45 -0
  492. package/src/ui/hooks/askSlot.ts +112 -0
  493. package/src/ui/hooks/useChat.js +271 -7
  494. package/src/ui/hooks/useChat.ts +1578 -0
  495. package/src/ui/hooks/useInputHistory.ts +235 -0
  496. package/src/ui/hooks/usePasteHandler.ts +220 -0
  497. package/src/ui/hooks/useTerminalSize.ts +41 -0
  498. package/src/ui/hooks/useTextBuffer.ts +108 -0
  499. package/src/ui/hooks/useTokenUsage.ts +44 -0
  500. package/src/ui/liveViewport.ts +189 -0
  501. package/src/ui/markdown.ts +230 -0
  502. package/src/ui/textBuffer.ts +239 -0
  503. package/src/ui/theme.ts +35 -0
  504. package/src/utils/clipboardImage.js +358 -0
  505. package/src/utils/clipboardImage.ts +420 -0
  506. package/src/utils/debugLog.ts +50 -0
  507. package/src/utils/greenRoot.ts +35 -0
  508. package/src/utils/packageRoot.ts +40 -0
  509. package/src/utils/pathContains.js +28 -0
  510. package/src/utils/pathContains.ts +31 -0
  511. package/src/utils/resourcePaths.ts +71 -0
  512. package/src/utils/stripThink.ts +49 -0
  513. package/src/utils/zodToJson.ts +73 -0
  514. package/src/web/browserBridge.js +365 -66
  515. package/src/web/browserBridge.ts +672 -0
  516. package/src/web/protocol.ts +1804 -0
  517. package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
  518. package/workflows/ppt.yaml +13 -2
  519. package/workflows/wechat-reading-shots.yaml +315 -0
  520. package/workflows/word.yaml +11 -1
  521. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  522. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  523. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  524. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  526. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  527. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  528. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  529. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  530. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  531. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  532. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  533. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  534. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  535. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  536. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  537. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  538. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  539. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  540. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  541. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  542. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  543. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  544. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  545. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  546. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  547. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  548. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  549. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  550. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  551. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  552. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  553. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  554. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  555. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  556. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  557. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  558. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  559. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  560. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  561. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  562. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  563. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  564. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  565. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  566. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  567. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  568. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  569. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  570. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  571. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  572. /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
  573. /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
@@ -0,0 +1,911 @@
1
+ /**
2
+ * ============================================================
3
+ * test/workflows/runner.test.ts
4
+ * ------------------------------------------------------------
5
+ * runner P0 行为:
6
+ * - tool / llm / assert step 成功路径
7
+ * - capture 绑变量到 VarStack
8
+ * - when=false → workflow_step_skipped
9
+ * - onError=halt(默认)→ workflow halt + error
10
+ * - onError=continue → 继续下一步
11
+ * - 工作流完成 yield workflow_done
12
+ *
13
+ * 策略:
14
+ * - tool 步骤用真实 Write 工具(写到 tmpdir,避免 mock executeTool)
15
+ * - llm 步骤 mock.module('../../../src/llm/client.js')
16
+ * - assert / when 走纯表达式引擎,无需 mock
17
+ * ============================================================
18
+ */
19
+
20
+ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ mkdtempSync,
25
+ readFileSync,
26
+ rmSync,
27
+ writeFileSync,
28
+ } from 'node:fs';
29
+ import { tmpdir } from 'node:os';
30
+ import { join } from 'node:path';
31
+
32
+ import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.js';
33
+ import { ALL_TOOLS } from '../../../src/tools/index.js';
34
+ import { runWorkflow } from '../src/runner.js';
35
+ import type {
36
+ LoopEvent,
37
+ Provider,
38
+ } from '../../../src/types.js';
39
+ import type {
40
+ RunContext,
41
+ WorkflowDef,
42
+ WorkflowEvent,
43
+ } from '../src/types.js';
44
+
45
+ type WfOrLoop = WorkflowEvent | LoopEvent;
46
+
47
+ const fakeProvider: Provider = {
48
+ name: 'test',
49
+ baseURL: '',
50
+ apiKey: '',
51
+ model: 'm',
52
+ contextWindow: 128000,
53
+ };
54
+
55
+ async function collect(
56
+ gen: AsyncGenerator<WfOrLoop, void, void>,
57
+ ): Promise<WfOrLoop[]> {
58
+ const out: WfOrLoop[] = [];
59
+ for await (const ev of gen) out.push(ev);
60
+ return out;
61
+ }
62
+
63
+ describe('workflows/runner', () => {
64
+ let tmp: string;
65
+ let ctx: RunContext;
66
+
67
+ beforeEach(() => {
68
+ tmp = mkdtempSync(join(tmpdir(), 'wf-runner-'));
69
+ process.env.MINIMAL_AGENT_CWD = tmp;
70
+ _resetWorkingDir();
71
+ // 抵抗 pluginRunner.test.ts 等遗留的 workingDir.ts mock 全局污染
72
+ mock.module('../../../src/bootstrap/workingDir.js', () => ({
73
+ getWorkingDir: () => tmp,
74
+ initWorkingDir: () => tmp,
75
+ _resetWorkingDir: () => {},
76
+ }));
77
+ ctx = { provider: fakeProvider, history: [], cwd: tmp, tools: ALL_TOOLS };
78
+ });
79
+
80
+ afterEach(() => {
81
+ delete process.env.MINIMAL_AGENT_CWD;
82
+ _resetWorkingDir();
83
+ rmSync(tmp, { recursive: true, force: true });
84
+ });
85
+
86
+ it('线性 assert step 全通过 → workflow_done', async () => {
87
+ const def: WorkflowDef = {
88
+ name: 'pure-assert',
89
+ description: 'd',
90
+ steps: [
91
+ { id: 's1', type: 'assert', condition: '1 == 1' },
92
+ { id: 's2', type: 'assert', condition: '"a" != "b"' },
93
+ ],
94
+ };
95
+ const events = await collect(runWorkflow(def, {}, ctx));
96
+ const types = events.map((e) => e.type);
97
+ expect(types[0]).toBe('workflow_start');
98
+ expect(types.filter((t) => t === 'workflow_step_end').length).toBe(2);
99
+ // v0.9 后 workflow 完成时可能在 workflow_done 前插入 assistant_message 等事件,
100
+ // 不再保证 workflow_done 严格是末位事件,改用 contains 检查存在性。
101
+ expect(types).toContain('workflow_done');
102
+ });
103
+
104
+ it('assert 失败 → halt + error 事件', async () => {
105
+ const def: WorkflowDef = {
106
+ name: 'assert-fail',
107
+ description: 'd',
108
+ steps: [
109
+ { id: 's1', type: 'assert', condition: '1 == 2', onFail: 'nope' },
110
+ { id: 's2', type: 'assert', condition: 'true' }, // 不应执行
111
+ ],
112
+ };
113
+ const events = await collect(runWorkflow(def, {}, ctx));
114
+ expect(events.some((e) => e.type === 'error')).toBe(true);
115
+ // s2 不应有 step_start
116
+ const startedIds = events
117
+ .filter((e): e is Extract<WorkflowEvent, { type: 'workflow_step_start' }> => e.type === 'workflow_step_start')
118
+ .map((e) => e.id);
119
+ expect(startedIds).toEqual(['s1']);
120
+ // 没 workflow_done
121
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
122
+ });
123
+
124
+ it('onError=continue → 失败后继续下一步', async () => {
125
+ const def: WorkflowDef = {
126
+ name: 'continue',
127
+ description: 'd',
128
+ steps: [
129
+ { id: 's1', type: 'assert', condition: 'false', onError: 'continue' },
130
+ { id: 's2', type: 'assert', condition: 'true' },
131
+ ],
132
+ };
133
+ const events = await collect(runWorkflow(def, {}, ctx));
134
+ const ends = events.filter(
135
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> => e.type === 'workflow_step_end',
136
+ );
137
+ expect(ends.length).toBe(2);
138
+ expect(ends[0].ok).toBe(false);
139
+ expect(ends[1].ok).toBe(true);
140
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
141
+ });
142
+
143
+ it('when=false → workflow_step_skipped 而不是 step_start', async () => {
144
+ const def: WorkflowDef = {
145
+ name: 'skip',
146
+ description: 'd',
147
+ inputs: [{ name: 'flag', type: 'string' }],
148
+ steps: [
149
+ {
150
+ id: 's1',
151
+ type: 'assert',
152
+ condition: 'true',
153
+ when: 'inputs.flag == "yes"',
154
+ },
155
+ { id: 's2', type: 'assert', condition: 'true' },
156
+ ],
157
+ };
158
+ const events = await collect(runWorkflow(def, { flag: 'no' }, ctx));
159
+ const skipped = events.find((e) => e.type === 'workflow_step_skipped');
160
+ expect(skipped).toBeDefined();
161
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
162
+ });
163
+
164
+ it('tool step 写文件 + ${var} 插值(用真实 Write 工具)', async () => {
165
+ const def: WorkflowDef = {
166
+ name: 'wr',
167
+ description: 'd',
168
+ inputs: [{ name: 'book', type: 'string' }],
169
+ steps: [
170
+ {
171
+ id: 'w',
172
+ tool: 'Write',
173
+ args: {
174
+ file_path: 'videos/${inputs.book}/x.txt',
175
+ content: 'hello ${inputs.book}',
176
+ },
177
+ },
178
+ ],
179
+ };
180
+ const events = await collect(runWorkflow(def, { book: '原则' }, ctx));
181
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
182
+
183
+ const filePath = join(tmp, 'videos', '原则', 'x.txt');
184
+ expect(existsSync(filePath)).toBe(true);
185
+ expect(readFileSync(filePath, 'utf8')).toBe('hello 原则');
186
+ });
187
+
188
+ it('声明 artifacts 但最终缺失时不能标记 workflow_done', async () => {
189
+ const def: WorkflowDef = {
190
+ name: 'missing-artifact',
191
+ description: 'd',
192
+ artifacts: ['expected.txt'],
193
+ steps: [{ id: 'noop', type: 'assert', condition: 'true' }],
194
+ };
195
+
196
+ const events = await collect(runWorkflow(def, {}, ctx));
197
+
198
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
199
+ expect(
200
+ events.some(
201
+ (e) =>
202
+ e.type === 'error' &&
203
+ e.error.includes('工作流声明的产物缺失') &&
204
+ e.error.includes('expected.txt'),
205
+ ),
206
+ ).toBe(true);
207
+ });
208
+
209
+ it('capture 把 tool 结果绑到变量,下一步 assert 能用', async () => {
210
+ const def: WorkflowDef = {
211
+ name: 'cap',
212
+ description: 'd',
213
+ steps: [
214
+ {
215
+ id: 'w',
216
+ tool: 'Write',
217
+ args: { file_path: 'a.txt', content: 'data' },
218
+ capture: { content: 'last_write_content' },
219
+ },
220
+ {
221
+ id: 'check',
222
+ type: 'assert',
223
+ condition: 'length(last_write_content) > 0',
224
+ },
225
+ ],
226
+ };
227
+ const events = await collect(runWorkflow(def, {}, ctx));
228
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
229
+ });
230
+
231
+ it('未知 tool → halt + error', async () => {
232
+ const def: WorkflowDef = {
233
+ name: 'badtool',
234
+ description: 'd',
235
+ steps: [{ id: 'x', tool: 'NoSuchTool9999' }],
236
+ };
237
+ const events = await collect(runWorkflow(def, {}, ctx));
238
+ expect(events.some((e) => e.type === 'error')).toBe(true);
239
+ });
240
+
241
+ it('llm step 拿 chat 累积文本 + capture text', async () => {
242
+ mock.module('../../../src/llm/client.js', () => ({
243
+ chat: async function* () {
244
+ yield { type: 'text_delta', delta: 'Hello ' };
245
+ yield { type: 'text_delta', delta: 'world' };
246
+ yield { type: 'done', stopReason: 'stop' };
247
+ },
248
+ }));
249
+
250
+ const def: WorkflowDef = {
251
+ name: 'llm-cap',
252
+ description: 'd',
253
+ steps: [
254
+ {
255
+ id: 'g',
256
+ llm: 'irrelevant',
257
+ capture: { text: 'msg' },
258
+ },
259
+ {
260
+ id: 'check',
261
+ type: 'assert',
262
+ condition: 'msg == "Hello world"',
263
+ },
264
+ ],
265
+ };
266
+ const events = await collect(runWorkflow(def, {}, ctx));
267
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
268
+ });
269
+
270
+ it('workflow_step_start 的 index 与 total 与 steps 长度一致', async () => {
271
+ const def: WorkflowDef = {
272
+ name: 'count',
273
+ description: 'd',
274
+ steps: [
275
+ { id: 'a', type: 'assert', condition: 'true' },
276
+ { id: 'b', type: 'assert', condition: 'true' },
277
+ { id: 'c', type: 'assert', condition: 'true' },
278
+ ],
279
+ };
280
+ const events = await collect(runWorkflow(def, {}, ctx));
281
+ const starts = events.filter(
282
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_start' }> => e.type === 'workflow_step_start',
283
+ );
284
+ expect(starts.length).toBe(3);
285
+ expect(starts.map((s) => s.index)).toEqual([0, 1, 2]);
286
+ expect(starts.every((s) => s.total === 3)).toBe(true);
287
+ });
288
+
289
+ it('signal.aborted → 立即 interrupted', async () => {
290
+ const ctrl = new AbortController();
291
+ ctrl.abort();
292
+ const def: WorkflowDef = {
293
+ name: 'abort',
294
+ description: 'd',
295
+ steps: [{ id: 'a', type: 'assert', condition: 'true' }],
296
+ };
297
+ const events = await collect(
298
+ runWorkflow(def, {}, { provider: fakeProvider, history: [], signal: ctrl.signal, cwd: tmp, tools: [] }),
299
+ );
300
+ expect(events.some((e) => e.type === 'interrupted')).toBe(true);
301
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
302
+ });
303
+
304
+ it('P1 branch step:condition=true → 走 then 分支,输出 branch=true', async () => {
305
+ const def: WorkflowDef = {
306
+ name: 'branch-true',
307
+ description: 'd',
308
+ inputs: [{ name: 'x', type: 'number' }],
309
+ steps: [
310
+ {
311
+ id: 'br',
312
+ type: 'branch',
313
+ condition: 'inputs.x > 0',
314
+ then: [
315
+ {
316
+ id: 'then_w',
317
+ tool: 'Write',
318
+ args: { file_path: 'pos.txt', content: 'positive' },
319
+ },
320
+ ],
321
+ else: [
322
+ {
323
+ id: 'else_w',
324
+ tool: 'Write',
325
+ args: { file_path: 'neg.txt', content: 'negative' },
326
+ },
327
+ ],
328
+ },
329
+ ],
330
+ };
331
+ const events = await collect(runWorkflow(def, { x: 5 }, ctx));
332
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
333
+ expect(existsSync(join(tmp, 'pos.txt'))).toBe(true);
334
+ expect(existsSync(join(tmp, 'neg.txt'))).toBe(false);
335
+ const ends = events.filter(
336
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> => e.type === 'workflow_step_end',
337
+ );
338
+ // 至少 then_w (1) + br (1) = 2 个 step_end,且都 ok
339
+ expect(ends.length).toBe(2);
340
+ expect(ends.every((e) => e.ok)).toBe(true);
341
+ const brEnd = ends.find((e) => e.id === 'br');
342
+ expect(brEnd?.output).toBe('branch=true');
343
+ });
344
+
345
+ it('P1 branch step:condition=false → 走 else 分支', async () => {
346
+ const def: WorkflowDef = {
347
+ name: 'branch-false',
348
+ description: 'd',
349
+ inputs: [{ name: 'x', type: 'number' }],
350
+ steps: [
351
+ {
352
+ id: 'br',
353
+ type: 'branch',
354
+ condition: 'inputs.x > 0',
355
+ then: [{ id: 'then_a', type: 'assert', condition: 'true' }],
356
+ else: [
357
+ {
358
+ id: 'else_w',
359
+ tool: 'Write',
360
+ args: { file_path: 'neg.txt', content: 'negative' },
361
+ },
362
+ ],
363
+ },
364
+ ],
365
+ };
366
+ const events = await collect(runWorkflow(def, { x: -3 }, ctx));
367
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
368
+ expect(existsSync(join(tmp, 'neg.txt'))).toBe(true);
369
+ const brEnd = events.find(
370
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
371
+ e.type === 'workflow_step_end' && e.id === 'br',
372
+ );
373
+ expect(brEnd?.output).toBe('branch=false');
374
+ });
375
+
376
+ it('P1 branch step:缺 then/else 也合法(cond 求值后空跳过)', async () => {
377
+ const def: WorkflowDef = {
378
+ name: 'branch-empty',
379
+ description: 'd',
380
+ steps: [
381
+ { id: 'br', type: 'branch', condition: '1 == 1' },
382
+ { id: 's2', type: 'assert', condition: 'true' },
383
+ ],
384
+ };
385
+ const events = await collect(runWorkflow(def, {}, ctx));
386
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
387
+ const ids = events
388
+ .filter((e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> => e.type === 'workflow_step_end')
389
+ .map((e) => e.id);
390
+ expect(ids).toEqual(['br', 's2']);
391
+ });
392
+
393
+ it('set 步类型保留:单一 ${expr} 保布尔/数字/数组,多片段仍字符串(E2)', async () => {
394
+ const def: WorkflowDef = {
395
+ name: 'typed-set',
396
+ description: 'd',
397
+ steps: [
398
+ {
399
+ id: 'init',
400
+ type: 'set',
401
+ set: { qualified: true, count: 7, label: 'x' },
402
+ },
403
+ {
404
+ id: 'copy',
405
+ type: 'set',
406
+ set: {
407
+ // 单一表达式 → 类型保留
408
+ final_qualified: '${qualified}',
409
+ next_count: '${count + 1}',
410
+ arr: "${split('a,b', ',')}",
411
+ // 多片段模板 → 仍是字符串(向后兼容)
412
+ summary: '${label}-${count}',
413
+ },
414
+ },
415
+ // 布尔直接 == true 可过(不再需要 == 'true' 防御)——这是 E2 的验收标准
416
+ { id: 'check_bool', type: 'assert', condition: 'final_qualified == true' },
417
+ { id: 'check_num', type: 'assert', condition: 'next_count == 8' },
418
+ { id: 'check_arr', type: 'assert', condition: 'length(arr) == 2' },
419
+ { id: 'check_str', type: 'assert', condition: "summary == 'x-7'" },
420
+ ],
421
+ };
422
+ const events = await collect(runWorkflow(def, {}, ctx));
423
+ expect(events.some((e) => e.type === 'error')).toBe(false);
424
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
425
+ });
426
+
427
+ it('set 步确定性提取:extract + join 从输出抽 <learning> 标签(E1 落地)', async () => {
428
+ const def: WorkflowDef = {
429
+ name: 'extract-learning',
430
+ description: 'd',
431
+ steps: [
432
+ {
433
+ id: 'fake_output',
434
+ type: 'set',
435
+ set: {
436
+ output:
437
+ '做完了 <learning>坑一</learning> 其它 <learning>坑二</learning>',
438
+ },
439
+ },
440
+ {
441
+ id: 'extract',
442
+ type: 'set',
443
+ set: {
444
+ learning: "${join(extract(output, '<learning>(.*?)</learning>'), '; ')}",
445
+ },
446
+ },
447
+ { id: 'check', type: 'assert', condition: "learning == '坑一; 坑二'" },
448
+ // 无标签时 extract → [] → join → ''(ralph-loop 的 when 守卫依赖这个语义)
449
+ {
450
+ id: 'extract_none',
451
+ type: 'set',
452
+ set: { none: "${join(extract('无标签文本', '<learning>(.*?)</learning>'), '; ')}" },
453
+ },
454
+ { id: 'check_none', type: 'assert', condition: "none == ''" },
455
+ ],
456
+ };
457
+ const events = await collect(runWorkflow(def, {}, ctx));
458
+ expect(events.some((e) => e.type === 'error')).toBe(false);
459
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
460
+ });
461
+
462
+ it('P1 loop step:over=数组字面量,子步骤用 ${as} 和 ${as}_idx', async () => {
463
+ const def: WorkflowDef = {
464
+ name: 'loop-lit',
465
+ description: 'd',
466
+ steps: [
467
+ {
468
+ id: 'lp',
469
+ type: 'loop',
470
+ over: '["a", "b", "c"]',
471
+ as: 'item',
472
+ do: [
473
+ {
474
+ id: 'w',
475
+ tool: 'Write',
476
+ args: {
477
+ file_path: 'items/${item_idx}-${item}.txt',
478
+ content: 'idx=${item_idx} val=${item}',
479
+ },
480
+ },
481
+ ],
482
+ },
483
+ ],
484
+ };
485
+ const events = await collect(runWorkflow(def, {}, ctx));
486
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
487
+ for (let i = 0; i < 3; i++) {
488
+ const val = ['a', 'b', 'c'][i];
489
+ const filePath = join(tmp, 'items', `${i}-${val}.txt`);
490
+ expect(existsSync(filePath)).toBe(true);
491
+ expect(readFileSync(filePath, 'utf8')).toBe(`idx=${i} val=${val}`);
492
+ }
493
+ const lpEnd = events.find(
494
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
495
+ e.type === 'workflow_step_end' && e.id === 'lp',
496
+ );
497
+ expect(lpEnd?.output).toBe('loop 3 次');
498
+ });
499
+
500
+ it('P1 loop step:迭代变量在循环外不再可见(push/pop 帧)', async () => {
501
+ const def: WorkflowDef = {
502
+ name: 'loop-scope',
503
+ description: 'd',
504
+ steps: [
505
+ {
506
+ id: 'lp',
507
+ type: 'loop',
508
+ over: '[1, 2]',
509
+ as: 'i',
510
+ do: [{ id: 'noop', type: 'assert', condition: 'i > 0' }],
511
+ },
512
+ // 循环外的 ${i} 已 pop —— 这里把它当字符串拿不到,length()=0
513
+ { id: 'check', type: 'assert', condition: 'length(i) == 0' },
514
+ ],
515
+ };
516
+ const events = await collect(runWorkflow(def, {}, ctx));
517
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
518
+ });
519
+
520
+ it('P1 loop step:over 求值不是数组 → halt + error', async () => {
521
+ const def: WorkflowDef = {
522
+ name: 'loop-bad-over',
523
+ description: 'd',
524
+ steps: [
525
+ {
526
+ id: 'lp',
527
+ type: 'loop',
528
+ over: '"not an array"',
529
+ as: 'i',
530
+ do: [{ id: 'inner', type: 'assert', condition: 'true' }],
531
+ },
532
+ ],
533
+ };
534
+ const events = await collect(runWorkflow(def, {}, ctx));
535
+ expect(events.some((e) => e.type === 'error')).toBe(true);
536
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
537
+ });
538
+
539
+ it('pause 真 HITL:非交互宿主 askBridge 自动选推荐项(继续)→ 放行', async () => {
540
+ // 不注入 executeTool:走真 AskUserQuestion → askBridge 无 UI handler → 自动选首项「继续执行 (推荐)」
541
+ const def: WorkflowDef = {
542
+ name: 'pause-approve',
543
+ description: 'd',
544
+ steps: [
545
+ { id: 'p1', type: 'pause', prompt: '等用户确认' },
546
+ { id: 's2', type: 'assert', condition: 'true' },
547
+ ],
548
+ };
549
+ const events = await collect(runWorkflow(def, {}, ctx));
550
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
551
+ const end = events.find(
552
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
553
+ e.type === 'workflow_step_end' && e.id === 'p1',
554
+ );
555
+ expect(end?.ok).toBe(true);
556
+ expect(end?.output).toContain('审批通过');
557
+ });
558
+
559
+ it('pause 真 HITL:用户选「停止」→ halt 且现场保留(可 resume)', async () => {
560
+ // DI executeTool:模拟用户在 AskUserQuestion 界面选了「停止(保留现场)」
561
+ ctx.executeTool = async (name: string) => {
562
+ expect(name).toBe('AskUserQuestion');
563
+ return { ok: true, content: '用户已回答 AskUserQuestion:\nA1: 停止(保留现场)' };
564
+ };
565
+ const def: WorkflowDef = {
566
+ name: 'pause-stop',
567
+ description: 'd',
568
+ steps: [
569
+ { id: 'gate', type: 'pause', prompt: '等审批' },
570
+ { id: 'after', type: 'assert', condition: 'true' }, // 不应执行
571
+ ],
572
+ };
573
+ const events = await collect(runWorkflow(def, {}, ctx));
574
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
575
+ const halt = events.find(
576
+ (e): e is { type: 'error'; error: string } =>
577
+ e.type === 'error' && e.error.includes('Workflow halted'),
578
+ );
579
+ expect(halt).toBeDefined();
580
+ expect(halt!.error).toContain('选择停止');
581
+ const startedIds = events
582
+ .filter(
583
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_start' }> =>
584
+ e.type === 'workflow_step_start',
585
+ )
586
+ .map((e) => e.id);
587
+ expect(startedIds).not.toContain('after');
588
+ });
589
+
590
+ it('pause 真 HITL:提示语含「停止」字样 + 用户选「继续」→ 必须放行(事故回归锚)', async () => {
591
+ // 真事故:plan.yaml 的 pause 提示语写着『选「停止」会保留现场…』,
592
+ // 回执的 Q 行带问题原文——曾因看整个 content 把用户选的「继续」误判成停止。
593
+ ctx.executeTool = async (_name: string, rawArgsJson: string) => {
594
+ const args = JSON.parse(rawArgsJson) as {
595
+ questions: { question: string }[];
596
+ };
597
+ // 模拟 ask.ts 的真实回执格式:Q 行含问题原文(内含「停止」)+ A 行是用户的选择
598
+ return {
599
+ ok: true,
600
+ content: `用户已回答 AskUserQuestion:\n\nQ1 [审批闸门] ${args.questions[0].question}\nA1: 继续执行 (推荐)`,
601
+ };
602
+ };
603
+ const def: WorkflowDef = {
604
+ name: 'pause-q-contains-stop',
605
+ description: 'd',
606
+ steps: [
607
+ {
608
+ id: 'gate',
609
+ type: 'pause',
610
+ prompt: '计划已生成。选「停止」会保留现场,稍后可从断点续跑。',
611
+ },
612
+ { id: 'after', type: 'assert', condition: 'true' },
613
+ ],
614
+ };
615
+ const events = await collect(runWorkflow(def, {}, ctx));
616
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
617
+ const end = events.find(
618
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
619
+ e.type === 'workflow_step_end' && e.id === 'gate',
620
+ );
621
+ expect(end?.ok).toBe(true);
622
+ });
623
+
624
+ it('pause 真 HITL:自由输入(其他逃生项)→ 不放行,原话进 halt 文案', async () => {
625
+ ctx.executeTool = async () => ({
626
+ ok: true,
627
+ content: '用户已回答 AskUserQuestion:\n\nQ1 [审批闸门] 是否继续?\nA1: 先把第3步的方案换成方案B',
628
+ });
629
+ const def: WorkflowDef = {
630
+ name: 'pause-freetext',
631
+ description: 'd',
632
+ steps: [{ id: 'gate', type: 'pause', prompt: '是否继续?' }],
633
+ };
634
+ const events = await collect(runWorkflow(def, {}, ctx));
635
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
636
+ const halt = events.find(
637
+ (e): e is { type: 'error'; error: string } =>
638
+ e.type === 'error' && e.error.includes('Workflow halted'),
639
+ );
640
+ expect(halt).toBeDefined();
641
+ expect(halt!.error).toContain('先把第3步的方案换成方案B');
642
+ });
643
+
644
+ it('pause 真 HITL:free_text=approve 时自由输入作为补充要求放行并 capture', async () => {
645
+ ctx.executeTool = async () => ({
646
+ ok: true,
647
+ content:
648
+ '用户已回答 AskUserQuestion:\n\nQ1 [审批闸门] 是否继续?\nA1: 确认使用 pptx skill,不要从头摸索 ppt 操作',
649
+ });
650
+ const def: WorkflowDef = {
651
+ name: 'pause-freetext-approve',
652
+ description: 'd',
653
+ steps: [
654
+ {
655
+ id: 'gate',
656
+ type: 'pause',
657
+ prompt: '是否继续?',
658
+ free_text: 'approve',
659
+ capture: { text: 'approval_notes' },
660
+ },
661
+ { id: 'after', type: 'assert', condition: "includes(approval_notes, 'pptx skill')" },
662
+ ],
663
+ };
664
+ const events = await collect(runWorkflow(def, {}, ctx));
665
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
666
+ const end = events.find(
667
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
668
+ e.type === 'workflow_step_end' && e.id === 'gate',
669
+ );
670
+ expect(end?.ok).toBe(true);
671
+ expect(end?.output).toContain('补充要求');
672
+ expect(end?.output).toContain('pptx skill');
673
+ });
674
+
675
+ // R1 减法重构:专用 approval_loop 步类型已删除(0 个 yaml 使用即孤儿)。
676
+ // 同一审批语义由通用组合表达(loop + pause(free_text:approve) + branch + llm 修订),
677
+ // 与 plan.yaml v4 的 stage4 审批环同范式——本测试锁定该组合的行为等价性。
678
+ it('plan 审批闭环(通用组合):自由输入先改 PLAN.md,再次明确继续后才执行', async () => {
679
+ let askCount = 0;
680
+ ctx.executeTool = async (name: string, rawArgsJson: string, signal?: AbortSignal) => {
681
+ if (name === 'AskUserQuestion') {
682
+ askCount += 1;
683
+ return {
684
+ ok: true,
685
+ content:
686
+ askCount === 1
687
+ ? '用户已回答 AskUserQuestion:\n\nQ1 [审批闸门] 是否继续?\nA1: 图形都用 svg,不要位图'
688
+ : '用户已回答 AskUserQuestion:\n\nQ1 [审批闸门] 是否继续?\nA1: 继续执行 (推荐)',
689
+ };
690
+ }
691
+ return (await import('../../../src/tools/index.js')).executeTool(name, rawArgsJson, signal);
692
+ };
693
+ ctx.chat = async function* () {
694
+ yield { type: 'text_delta', delta: '最终计划:所有图形都用 svg\n## 产出物清单\n- [ ] executed.txt' };
695
+ yield { type: 'done', stopReason: 'stop' };
696
+ };
697
+
698
+ const def: WorkflowDef = {
699
+ name: 'plan-approval-loop',
700
+ description: 'd',
701
+ artifacts: ['PLAN.md', 'executed.txt'],
702
+ steps: [
703
+ { id: 'init', type: 'set', set: { final_plan_text: '初版计划:图形随意', plan_approved: false, approval_notes: '' } },
704
+ {
705
+ id: 'write_plan',
706
+ tool: 'Write',
707
+ args: { file_path: 'PLAN.md', content: '${final_plan_text}' },
708
+ artifacts: ['PLAN.md'],
709
+ },
710
+ {
711
+ id: 'approval',
712
+ type: 'loop',
713
+ max_iterations: 3,
714
+ do: [
715
+ {
716
+ id: 'gate',
717
+ type: 'pause',
718
+ prompt: '是否继续执行 PLAN.md?',
719
+ free_text: 'approve',
720
+ capture: { text: 'approval_notes' },
721
+ },
722
+ {
723
+ id: 'route',
724
+ type: 'branch',
725
+ condition: "approval_notes == ''",
726
+ then: [
727
+ { id: 'mark_approved', type: 'set', set: { plan_approved: 'true' } },
728
+ { id: 'approved_break', type: 'break' },
729
+ ],
730
+ else: [
731
+ {
732
+ id: 'revise',
733
+ llm: '把用户反馈合入计划:${approval_notes}\n当前计划:${final_plan_text}',
734
+ capture: { text: 'final_plan_text' },
735
+ },
736
+ {
737
+ id: 'revise_write',
738
+ tool: 'Write',
739
+ args: { file_path: 'PLAN.md', content: '${final_plan_text}' },
740
+ artifacts: ['PLAN.md'],
741
+ },
742
+ { id: 'clear_notes', type: 'set', set: { approval_notes: '' } },
743
+ ],
744
+ },
745
+ ],
746
+ },
747
+ {
748
+ id: 'ensure_approved',
749
+ type: 'assert',
750
+ condition: "plan_approved == true || plan_approved == 'true'",
751
+ },
752
+ {
753
+ id: 'execute',
754
+ tool: 'Write',
755
+ args: { file_path: 'executed.txt', content: 'executed after approval' },
756
+ artifacts: ['executed.txt'],
757
+ },
758
+ ],
759
+ };
760
+
761
+ const events = await collect(runWorkflow(def, {}, ctx));
762
+
763
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
764
+ expect(askCount).toBe(2);
765
+ expect(readFileSync(join(tmp, 'PLAN.md'), 'utf8')).toContain('svg');
766
+ expect(readFileSync(join(tmp, 'executed.txt'), 'utf8')).toBe('executed after approval');
767
+
768
+ const ended = events
769
+ .filter(
770
+ (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
771
+ e.type === 'workflow_step_end',
772
+ )
773
+ .map((e) => e.id);
774
+ expect(ended.indexOf('revise_write')).toBeLessThan(ended.indexOf('execute'));
775
+ });
776
+
777
+ it('pause 真 HITL:用户取消提问(ESC)→ 不放行(审批门上取消 = 不批准)', async () => {
778
+ ctx.executeTool = async () => ({ ok: false, error: '提问被取消 / 中断:esc' });
779
+ const def: WorkflowDef = {
780
+ name: 'pause-cancel',
781
+ description: 'd',
782
+ steps: [{ id: 'gate', type: 'pause', prompt: '等审批' }],
783
+ };
784
+ const events = await collect(runWorkflow(def, {}, ctx));
785
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
786
+ expect(
787
+ events.some(
788
+ (e) => e.type === 'error' && e.error.includes('选择停止'),
789
+ ),
790
+ ).toBe(true);
791
+ });
792
+
793
+ it('P1 TUI 可见:每个 step 镜像 plugin_progress,tool 步骤发 tool_start/tool_end', async () => {
794
+ const def: WorkflowDef = {
795
+ name: 'visible',
796
+ description: 'd',
797
+ steps: [
798
+ { id: 'w', tool: 'Write', args: { file_path: 'out.txt', content: 'hi' } },
799
+ { id: 'a', type: 'assert', condition: 'true' },
800
+ ],
801
+ };
802
+ const events = await collect(runWorkflow(def, {}, ctx));
803
+ const types = events.map((e) => e.type);
804
+
805
+ // 通用进度事件:开工(0/2) + 2 个 step 级 + 完成(2/2) = 4 条 plugin_progress
806
+ const pp = events.filter(
807
+ (e): e is Extract<LoopEvent, { type: 'plugin_progress' }> =>
808
+ e.type === 'plugin_progress',
809
+ );
810
+ expect(pp.length).toBe(4);
811
+ expect(pp.every((e) => e.pluginId === 'workflow-runner')).toBe(true);
812
+ expect(pp.some((e) => e.message?.includes('step w (tool)'))).toBe(true);
813
+
814
+ // tool 步骤像普通 ReAct 工具调用一样可见
815
+ expect(types.filter((t) => t === 'tool_start').length).toBe(1);
816
+ expect(types.filter((t) => t === 'tool_end').length).toBe(1);
817
+ const ts = events.find(
818
+ (e): e is Extract<LoopEvent, { type: 'tool_start' }> =>
819
+ e.type === 'tool_start',
820
+ );
821
+ expect(ts?.toolName).toBe('Write');
822
+
823
+ // 私有 workflow_* 仍在(-p / 既有 e2e 测试依赖),但末位可能因 v0.9 重构后
824
+ // runWorkflow 补发 assistant_message 而非严格以 workflow_done 收尾。
825
+ expect(types[0]).toBe('workflow_start');
826
+ expect(types).toContain('workflow_done');
827
+ expect(existsSync(join(tmp, 'out.txt'))).toBe(true);
828
+ });
829
+
830
+ it('P1 skill step:加载 cwd/skills/<name>/SKILL.md,跑 runQuery 累计 text', async () => {
831
+ // 在 tmp 准备一个最小 SKILL.md
832
+ const skillDir = join(tmp, 'skills', 'echo-bot');
833
+ mkdirSync(skillDir, { recursive: true });
834
+ writeFileSync(
835
+ join(skillDir, 'SKILL.md'),
836
+ '---\nname: echo-bot\ndescription: echo\n---\n\n按用户指示原样输出',
837
+ 'utf8',
838
+ );
839
+
840
+ // skill executor 经真实 runQuery(TAOR) 执行;用 ctx.chat DI 注入 stub chat,
841
+ // 让真实 runQuery 真把 assistant 消息推进 ctx.history(skill.ts 据此提取结果)。
842
+ // 不再 mock.module(loop.js)——那是空壳 runQuery 不推 history(提取空、必失败),
843
+ // 且会全局泄漏污染其它测试文件(recovery 等的 runRecovery 也走 runQuery)。
844
+ ctx.chat = async function* () {
845
+ yield { type: 'text_delta', delta: 'echoed-' };
846
+ yield { type: 'text_delta', delta: 'OK' };
847
+ yield { type: 'done', stopReason: 'stop' };
848
+ };
849
+
850
+ const def: WorkflowDef = {
851
+ name: 'skill-test',
852
+ description: 'd',
853
+ steps: [
854
+ {
855
+ id: 'sk',
856
+ skill: 'echo-bot',
857
+ input: 'say hi',
858
+ capture: { text: 'msg' },
859
+ },
860
+ { id: 'check', type: 'assert', condition: 'msg == "echoed-OK"' },
861
+ ],
862
+ };
863
+ const events = await collect(runWorkflow(def, {}, ctx));
864
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
865
+ });
866
+
867
+ it('P1 skill step:未找到 skill → halt + error', async () => {
868
+ const def: WorkflowDef = {
869
+ name: 'skill-missing',
870
+ description: 'd',
871
+ steps: [{ id: 'sk', skill: 'definitely-not-exists' }],
872
+ };
873
+ const events = await collect(runWorkflow(def, {}, ctx));
874
+ expect(events.some((e) => e.type === 'error')).toBe(true);
875
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
876
+ });
877
+
878
+ it('onError:continue 降级 → capture 绑明确占位文本(非空串)+ warning 可见化', async () => {
879
+ // chat 永远 throw → llm 步空产出抛异常 → max_retries=0 直接落 onError:continue。
880
+ // 此前降级完全静默且 capture 留空:下游 prompt 内插 ${req} 得空段,模型困惑。
881
+ ctx.chat = async function* () {
882
+ throw new Error('provider down');
883
+ // biome-ignore lint/correctness/noUnreachable: stub
884
+ yield { type: 'done', stopReason: 'stop' };
885
+ };
886
+
887
+ const def: WorkflowDef = {
888
+ name: 'continue-placeholder',
889
+ description: 'd',
890
+ steps: [
891
+ {
892
+ id: 'g',
893
+ llm: 'do it',
894
+ capture: { text: 'req' },
895
+ max_retries: 0,
896
+ onError: 'continue',
897
+ },
898
+ // 占位文本生效:req 非空且含机器可识别前缀
899
+ { id: 'check', type: 'assert', condition: 'includes(req, "上游步骤 g 失败")' },
900
+ ],
901
+ };
902
+ const events = await collect(runWorkflow(def, {}, ctx));
903
+
904
+ expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
905
+ expect(
906
+ events.some(
907
+ (e) => e.type === 'warning' && e.message.includes('降级'),
908
+ ),
909
+ ).toBe(true);
910
+ });
911
+ });