minimal-agent 0.12.0 → 0.14.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 (472) hide show
  1. package/README.md +6 -5
  2. package/package.json +62 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +1 -1
  4. package/plugins/cron-runner/src/commands.js +4 -4
  5. package/plugins/cron-runner/src/commands.ts +4 -4
  6. package/plugins/workflow-runner/.claude-plugin/plugin.json +2 -2
  7. package/plugins/workflow-runner/commands/workflow.md +57 -90
  8. package/plugins/workflow-runner/commands/workflows.md +4 -4
  9. package/plugins/workflow-runner/plugin.js +24 -13
  10. package/plugins/workflow-runner/plugin.ts +28 -13
  11. package/plugins/workflow-runner/src/discovery.js +160 -0
  12. package/plugins/workflow-runner/src/discovery.ts +194 -0
  13. package/plugins/workflow-runner/src/goalAnchor.js +53 -0
  14. package/plugins/workflow-runner/src/goalAnchor.ts +59 -0
  15. package/plugins/workflow-runner/src/index.js +204 -185
  16. package/plugins/workflow-runner/src/index.ts +249 -224
  17. package/plugins/workflow-runner/src/inputCoercion.js +40 -0
  18. package/plugins/workflow-runner/src/inputCoercion.ts +43 -0
  19. package/plugins/workflow-runner/src/journal.js +224 -0
  20. package/plugins/workflow-runner/src/journal.ts +274 -0
  21. package/plugins/workflow-runner/src/jsonSchema.js +132 -0
  22. package/plugins/workflow-runner/src/jsonSchema.ts +162 -0
  23. package/plugins/workflow-runner/src/moduleResolution.js +64 -0
  24. package/plugins/workflow-runner/src/moduleResolution.ts +71 -0
  25. package/plugins/workflow-runner/src/resourceInputs.js +220 -0
  26. package/plugins/workflow-runner/src/resourceInputs.ts +300 -0
  27. package/plugins/workflow-runner/src/runner.js +404 -467
  28. package/plugins/workflow-runner/src/runner.ts +491 -535
  29. package/plugins/workflow-runner/src/runtime/config.js +46 -0
  30. package/plugins/workflow-runner/src/runtime/config.ts +69 -0
  31. package/plugins/workflow-runner/src/runtime/context.js +1304 -0
  32. package/plugins/workflow-runner/src/runtime/context.ts +1786 -0
  33. package/plugins/workflow-runner/src/runtime/exec.js +34 -0
  34. package/plugins/workflow-runner/src/runtime/exec.ts +53 -0
  35. package/plugins/workflow-runner/src/runtime/flow.js +8 -0
  36. package/plugins/workflow-runner/src/runtime/flow.ts +12 -0
  37. package/plugins/workflow-runner/src/runtime/fs.js +27 -0
  38. package/plugins/workflow-runner/src/runtime/fs.ts +41 -0
  39. package/plugins/workflow-runner/src/runtime/model.js +6 -0
  40. package/plugins/workflow-runner/src/runtime/model.ts +8 -0
  41. package/plugins/workflow-runner/src/runtime/output.js +7 -0
  42. package/plugins/workflow-runner/src/runtime/output.ts +21 -0
  43. package/plugins/workflow-runner/src/runtime/scheduler.js +141 -0
  44. package/plugins/workflow-runner/src/runtime/scheduler.ts +166 -0
  45. package/plugins/workflow-runner/src/runtime/state.js +16 -0
  46. package/plugins/workflow-runner/src/runtime/state.ts +19 -0
  47. package/plugins/workflow-runner/src/runtime/tools.js +3 -0
  48. package/plugins/workflow-runner/src/runtime/tools.ts +6 -0
  49. package/plugins/workflow-runner/src/sandbox.js +141 -0
  50. package/plugins/workflow-runner/src/sandbox.ts +182 -0
  51. package/plugins/workflow-runner/src/scriptGuard.d.mts +38 -0
  52. package/plugins/workflow-runner/src/scriptGuard.mjs +449 -0
  53. package/plugins/workflow-runner/src/taskIdentity.js +172 -152
  54. package/plugins/workflow-runner/src/taskIdentity.ts +233 -200
  55. package/plugins/workflow-runner/src/toolRuntime.js +29 -8
  56. package/plugins/workflow-runner/src/toolRuntime.ts +36 -11
  57. package/plugins/workflow-runner/src/types.js +7 -105
  58. package/plugins/workflow-runner/src/types.ts +155 -520
  59. package/plugins/workflow-runner/src/util.js +47 -0
  60. package/plugins/workflow-runner/src/util.ts +51 -0
  61. package/plugins/workflow-runner/src/workflowContext.js +20 -0
  62. package/plugins/workflow-runner/src/workflowContext.ts +58 -0
  63. package/plugins/workflow-runner/src/workflowDependencies.js +36 -0
  64. package/plugins/workflow-runner/src/workflowDependencies.ts +59 -0
  65. package/plugins/workflow-runner/src/workflowFingerprint.js +118 -0
  66. package/plugins/workflow-runner/src/workflowFingerprint.ts +122 -0
  67. package/plugins/workflow-runner/src/workflowState.js +542 -469
  68. package/plugins/workflow-runner/src/workflowState.ts +639 -589
  69. package/plugins/workflow-runner/test/api.test.ts +223 -0
  70. package/plugins/workflow-runner/test/apiCapabilities.test.ts +614 -0
  71. package/plugins/workflow-runner/test/apiConcurrencyGate.test.ts +104 -0
  72. package/plugins/workflow-runner/test/apiEcho.test.ts +210 -0
  73. package/plugins/workflow-runner/test/apiReadText.test.ts +178 -0
  74. package/plugins/workflow-runner/test/apiResourceLedger.test.ts +184 -0
  75. package/plugins/workflow-runner/test/childWorkflow.test.ts +304 -0
  76. package/plugins/workflow-runner/test/discovery.test.ts +234 -0
  77. package/plugins/workflow-runner/test/goalAnchor.test.ts +106 -0
  78. package/plugins/workflow-runner/test/goalController.test.ts +317 -0
  79. package/plugins/workflow-runner/test/journalLanes.test.ts +198 -0
  80. package/plugins/workflow-runner/test/listResources.test.ts +106 -0
  81. package/plugins/workflow-runner/test/resourceInputs.test.ts +294 -0
  82. package/plugins/workflow-runner/test/sandbox.test.ts +126 -0
  83. package/plugins/workflow-runner/test/scheduler.test.ts +70 -0
  84. package/plugins/workflow-runner/test/scriptGuard.test.ts +276 -0
  85. package/plugins/workflow-runner/test/skillDocSync.test.ts +78 -0
  86. package/plugins/workflow-runner/test/taskIdentity.test.ts +288 -179
  87. package/plugins/workflow-runner/test/workflowFingerprint.test.ts +106 -0
  88. package/plugins/workflow-runner/test/workflowState.test.ts +429 -0
  89. package/skills/pptx/examples/assets/cover-bg.svg +23 -0
  90. package/skills/pptx/examples/assets/icon-spark.svg +5 -0
  91. package/skills/skill-creator/scripts/run_eval.py +3 -2
  92. package/skills/ui-design/SKILL.md +5 -2
  93. package/skills/ui-design/references/layout-composition.md +1 -1
  94. package/skills/workflow-creator/SKILL.md +344 -0
  95. package/skills/workflow-creator/assets/template/workflow.mjs +48 -0
  96. package/skills/workflow-creator/references/api.md +216 -0
  97. package/skills/workflow-creator/references/migration.md +103 -0
  98. package/skills/workflow-creator/references/patterns.md +779 -0
  99. package/skills/workflow-creator/scripts/list-resources.mjs +205 -0
  100. package/skills/workflow-creator/scripts/scaffold.mjs +148 -0
  101. package/skills/workflow-creator/scripts/validate.mjs +275 -0
  102. package/skills/workflow-creator/scripts/workflow-creator.test.mjs +82 -0
  103. package/src/advisor/advisorCoordinator.js +503 -0
  104. package/src/advisor/advisorCoordinator.ts +657 -0
  105. package/src/cli/configMenu.tsx +0 -1
  106. package/src/cli/print.js +44 -6
  107. package/src/cli/print.ts +73 -4
  108. package/src/cli/streamJson.js +5 -6
  109. package/src/cli/streamJson.ts +8 -4
  110. package/src/cli/wizards/advisorWizard.js +4 -2
  111. package/src/cli/wizards/advisorWizard.tsx +8 -2
  112. package/src/cli/wizards/contextAdvisorWizard.js +5 -4
  113. package/src/cli/wizards/contextAdvisorWizard.tsx +7 -6
  114. package/src/collaboration/taskFlow.js +115 -0
  115. package/src/collaboration/taskFlow.ts +169 -0
  116. package/src/config/configDefaults.js +41 -1
  117. package/src/config/configDefaults.ts +43 -1
  118. package/src/config/configDiagnostics.ts +2 -2
  119. package/src/config/configFile.js +40 -9
  120. package/src/config/configFile.ts +60 -11
  121. package/src/config/configService.js +443 -74
  122. package/src/config/configService.ts +723 -142
  123. package/src/config/numericLimits.js +22 -0
  124. package/src/config/numericLimits.ts +24 -0
  125. package/src/config.js +1 -0
  126. package/src/config.ts +3 -2
  127. package/src/context/budgetPlan.js +1 -1
  128. package/src/context/budgetPlan.ts +1 -1
  129. package/src/context/compact.js +7 -2
  130. package/src/context/compact.ts +9 -2
  131. package/src/context/microCompactLite.js +212 -13
  132. package/src/context/microCompactLite.ts +261 -12
  133. package/src/context/sessionRegistry.js +8 -1
  134. package/src/context/sessionRegistry.ts +18 -5
  135. package/src/context/toolResultArchive.js +72 -0
  136. package/src/context/toolResultArchive.ts +95 -0
  137. package/src/goal/errors.js +8 -0
  138. package/src/goal/errors.ts +18 -0
  139. package/src/goal/goalRuntime.js +344 -0
  140. package/src/goal/goalRuntime.ts +466 -0
  141. package/src/goal/goalStore.js +200 -0
  142. package/src/goal/goalStore.ts +219 -0
  143. package/src/goal/index.js +4 -0
  144. package/src/goal/index.ts +4 -0
  145. package/src/goal/statusMapping.js +71 -0
  146. package/src/goal/statusMapping.ts +95 -0
  147. package/src/goal/types.js +7 -0
  148. package/src/goal/types.ts +179 -0
  149. package/src/goal/workflowController.js +306 -0
  150. package/src/goal/workflowController.ts +360 -0
  151. package/src/harness/evidenceLedger.js +2 -0
  152. package/src/harness/evidenceLedger.ts +1 -0
  153. package/src/harness/stallGuard.js +9 -3
  154. package/src/harness/stallGuard.ts +7 -3
  155. package/src/images/imageResolver.js +161 -0
  156. package/src/images/imageResolver.ts +189 -0
  157. package/src/infra/verification.js +56 -20
  158. package/src/infra/verification.ts +87 -20
  159. package/src/llm/client.js +79 -21
  160. package/src/llm/client.ts +182 -35
  161. package/src/llm/gatewayClient.js +201 -0
  162. package/src/llm/gatewayClient.ts +299 -0
  163. package/src/llm/messageProvenance.js +46 -0
  164. package/src/llm/messageProvenance.ts +58 -0
  165. package/src/llm/modelProvider.js +1 -0
  166. package/src/llm/modelProvider.ts +22 -0
  167. package/src/llm/providers/anthropicFormat.js +108 -1
  168. package/src/llm/providers/anthropicFormat.ts +115 -4
  169. package/src/llm/providers/openaiFormat.js +9 -0
  170. package/src/llm/providers/openaiFormat.ts +12 -3
  171. package/src/loop.js +319 -54
  172. package/src/loop.ts +373 -57
  173. package/src/main.js +4 -1
  174. package/src/main.tsx +4 -1
  175. package/src/managedExecution/index.js +2 -0
  176. package/src/managedExecution/index.ts +2 -0
  177. package/src/managedExecution/registry.js +15 -0
  178. package/src/managedExecution/registry.ts +20 -0
  179. package/src/managedExecution/types.js +1 -0
  180. package/src/managedExecution/types.ts +86 -0
  181. package/src/modelRef.js +9 -0
  182. package/src/modelRef.ts +31 -0
  183. package/src/multimodal.js +14 -28
  184. package/src/multimodal.ts +17 -33
  185. package/src/net/lanDiscovery.js +65 -0
  186. package/src/net/lanDiscovery.ts +82 -0
  187. package/src/plugin-sdk.js +28 -3
  188. package/src/plugin-sdk.ts +39 -6
  189. package/src/plugins/pluginApi.ts +16 -1
  190. package/src/plugins/pluginLoader.js +4 -0
  191. package/src/plugins/pluginLoader.ts +6 -0
  192. package/src/plugins/pluginRunner.js +152 -9
  193. package/src/plugins/pluginRunner.ts +186 -8
  194. package/src/plugins/slashFallback.js +113 -0
  195. package/src/plugins/slashFallback.ts +137 -0
  196. package/src/prompts/policyRegistry.js +15 -8
  197. package/src/prompts/policyRegistry.ts +18 -8
  198. package/src/prompts/promptAssembly.js +103 -12
  199. package/src/prompts/promptAssembly.ts +114 -12
  200. package/src/prompts/skillList.js +21 -3
  201. package/src/prompts/skillList.ts +27 -2
  202. package/src/prompts/stableSystemPrompt.js +18 -0
  203. package/src/prompts/stableSystemPrompt.ts +33 -0
  204. package/src/runtime/agentRuntimeContext.js +8 -2
  205. package/src/runtime/agentRuntimeContext.ts +17 -4
  206. package/src/session/runAgentTurn.js +78 -12
  207. package/src/session/runAgentTurn.ts +105 -23
  208. package/src/session/runTurn.js +10 -9
  209. package/src/session/runTurn.ts +17 -8
  210. package/src/session/sessionState.ts +2 -0
  211. package/src/team/auth.js +186 -0
  212. package/src/team/auth.ts +298 -0
  213. package/src/team/cli.js +105 -0
  214. package/src/team/cli.ts +122 -0
  215. package/src/team/onlineToolContract.js +64 -0
  216. package/src/team/onlineToolContract.ts +109 -0
  217. package/src/team/toolPolicy.js +36 -0
  218. package/src/team/toolPolicy.ts +75 -0
  219. package/src/tools/advisor/advisor.js +148 -37
  220. package/src/tools/advisor/advisor.ts +161 -38
  221. package/src/tools/advisor/advisorDecision.js +71 -0
  222. package/src/tools/advisor/advisorDecision.ts +85 -0
  223. package/src/tools/agent/agent.js +18 -5
  224. package/src/tools/agent/agent.ts +21 -7
  225. package/src/tools/agent/agentControl.js +246 -0
  226. package/src/tools/agent/agentControl.ts +284 -0
  227. package/src/tools/agent/agentTaskService.js +40 -1
  228. package/src/tools/agent/agentTaskService.ts +44 -1
  229. package/src/tools/agent/agentTypes.js +14 -2
  230. package/src/tools/agent/agentTypes.ts +15 -2
  231. package/src/tools/applypatch/applyPatch.js +362 -0
  232. package/src/tools/applypatch/applyPatch.ts +428 -0
  233. package/src/tools/bash/bash.js +70 -18
  234. package/src/tools/bash/bash.ts +75 -20
  235. package/src/tools/capabilities.js +10 -5
  236. package/src/tools/capabilities.ts +10 -5
  237. package/src/tools/computeruse/computerIntent.js +32 -0
  238. package/src/tools/computeruse/computerIntent.ts +46 -0
  239. package/src/tools/computeruse/telemetryLog.js +12 -0
  240. package/src/tools/computeruse/telemetryLog.ts +16 -0
  241. package/src/tools/computeruse/vision/telemetry.js +1 -0
  242. package/src/tools/computeruse/vision/telemetry.ts +1 -0
  243. package/src/tools/effectiveToolSet.js +4 -0
  244. package/src/tools/effectiveToolSet.ts +4 -0
  245. package/src/tools/index.js +21 -2
  246. package/src/tools/index.ts +33 -2
  247. package/src/tools/powershell/powershell.js +18 -1
  248. package/src/tools/powershell/powershell.ts +18 -2
  249. package/src/tools/presentresult/presentresult.js +6 -15
  250. package/src/tools/presentresult/presentresult.ts +6 -15
  251. package/src/tools/read/read.js +69 -11
  252. package/src/tools/read/read.ts +70 -11
  253. package/src/tools/shared/fileUtils.js +1 -2
  254. package/src/tools/shared/fileUtils.ts +1 -2
  255. package/src/tools/shared/permanentDeleteGuard.js +34 -3
  256. package/src/tools/shared/permanentDeleteGuard.ts +43 -6
  257. package/src/tools/shared/toolOutputStore.js +23 -1
  258. package/src/tools/shared/toolOutputStore.ts +22 -1
  259. package/src/tools/skill/skill.js +9 -3
  260. package/src/tools/skill/skill.ts +8 -4
  261. package/src/tools/toolPromptGuides.js +117 -0
  262. package/src/tools/toolPromptGuides.ts +137 -0
  263. package/src/tools/webbrowser/agentBrowserPath.js +19 -9
  264. package/src/tools/webbrowser/agentBrowserPath.ts +70 -28
  265. package/src/tools/webbrowser/browser.js +27 -11
  266. package/src/tools/webbrowser/browser.ts +68 -24
  267. package/src/tools/webbrowser/browserAutomation.js +329 -0
  268. package/src/tools/webbrowser/browserAutomation.ts +467 -0
  269. package/src/tools/webbrowser/browserAxState.js +97 -0
  270. package/src/tools/webbrowser/browserAxState.ts +142 -0
  271. package/src/tools/webbrowser/browserBatch.js +1578 -0
  272. package/src/tools/webbrowser/browserBatch.ts +2382 -0
  273. package/src/tools/webbrowser/browserCdp.js +208 -0
  274. package/src/tools/webbrowser/browserCdp.ts +255 -0
  275. package/src/tools/webbrowser/browserConditions.js +24 -0
  276. package/src/tools/webbrowser/browserConditions.ts +45 -0
  277. package/src/tools/webbrowser/browserContent.js +281 -0
  278. package/src/tools/webbrowser/browserContent.ts +438 -0
  279. package/src/tools/webbrowser/browserControl.js +131 -30
  280. package/src/tools/webbrowser/browserControl.ts +210 -50
  281. package/src/tools/webbrowser/browserDaemon.js +22 -14
  282. package/src/tools/webbrowser/browserDaemon.ts +54 -21
  283. package/src/tools/webbrowser/browserFrames.js +47 -0
  284. package/src/tools/webbrowser/browserFrames.ts +54 -0
  285. package/src/tools/webbrowser/browserInteractionResolver.js +17 -0
  286. package/src/tools/webbrowser/browserInteractionResolver.ts +40 -0
  287. package/src/tools/webbrowser/browserLocator.js +140 -0
  288. package/src/tools/webbrowser/browserLocator.ts +194 -0
  289. package/src/tools/webbrowser/browserOutcome.js +10 -17
  290. package/src/tools/webbrowser/browserOutcome.ts +48 -31
  291. package/src/tools/webbrowser/browserRecovery.js +58 -0
  292. package/src/tools/webbrowser/browserRecovery.ts +83 -0
  293. package/src/tools/webbrowser/browserRunRegistry.js +35 -3
  294. package/src/tools/webbrowser/browserRunRegistry.ts +91 -15
  295. package/src/tools/webbrowser/browserService.js +28 -2
  296. package/src/tools/webbrowser/browserService.ts +49 -9
  297. package/src/tools/webbrowser/browserStream.js +132 -33
  298. package/src/tools/webbrowser/browserStream.ts +224 -50
  299. package/src/tools/webbrowser/browserTabs.js +48 -15
  300. package/src/tools/webbrowser/browserTabs.ts +178 -96
  301. package/src/tools/webbrowser/webbrowser.js +683 -144
  302. package/src/tools/webbrowser/webbrowser.ts +979 -236
  303. package/src/tools/webfetch/preapproved.js +0 -2
  304. package/src/tools/webfetch/preapproved.ts +0 -2
  305. package/src/tools/websearch/websearch.js +129 -8
  306. package/src/tools/websearch/websearch.ts +138 -6
  307. package/src/types.ts +450 -42
  308. package/src/ui/App.js +377 -58
  309. package/src/ui/App.tsx +576 -111
  310. package/src/ui/LiveArea.js +14 -8
  311. package/src/ui/LiveArea.tsx +14 -10
  312. package/src/ui/MessageList.js +63 -20
  313. package/src/ui/MessageList.tsx +101 -16
  314. package/src/ui/ModelPicker.js +7 -4
  315. package/src/ui/ModelPicker.tsx +55 -8
  316. package/src/ui/hooks/useChat.js +152 -163
  317. package/src/ui/hooks/useChat.ts +396 -334
  318. package/src/ui/loginCommand.js +56 -0
  319. package/src/ui/loginCommand.ts +63 -0
  320. package/src/ui/toolProgress.js +41 -0
  321. package/src/ui/toolProgress.ts +44 -0
  322. package/src/web/browserBridge.js +20 -2
  323. package/src/web/browserBridge.ts +20 -2
  324. package/src/web/protocol.ts +1897 -194
  325. package/workflows/adaptive-research/workflow.mjs +86 -0
  326. package/workflows/e2e-capabilities/workflow.mjs +102 -0
  327. package/workflows/e2e-llm/workflow.mjs +52 -0
  328. package/workflows/e2e-smoke/workflow.mjs +59 -0
  329. package/workflows/e2e-vision-schema/README.md +48 -0
  330. package/workflows/e2e-vision-schema/workflow.mjs +149 -0
  331. package/workflows/e2e-write-greet/workflow.mjs +37 -0
  332. package/workflows/file-census/README.md +14 -0
  333. package/workflows/file-census/scripts/.gitkeep +2 -0
  334. package/workflows/file-census/workflow.mjs +110 -0
  335. package/workflows/goal/README.md +166 -0
  336. package/workflows/goal/prompts/audit.md +57 -0
  337. package/workflows/goal/prompts/contract.md +56 -0
  338. package/workflows/goal/prompts/iterate.md +37 -0
  339. package/workflows/goal/workflow.mjs +501 -0
  340. package/workflows/illustrate/README.md +90 -0
  341. package/workflows/illustrate/assets/engines.json +20 -0
  342. package/workflows/illustrate/assets/extract-baidu.js +21 -0
  343. package/workflows/illustrate/assets/extract-bing.js +33 -0
  344. package/workflows/illustrate/assets/extract-google.js +31 -0
  345. package/workflows/illustrate/workflow.mjs +301 -0
  346. package/workflows/plan/workflow.mjs +389 -0
  347. package/workflows/question-crops/README.md +35 -0
  348. package/workflows/question-crops/prompts/inventory.md +23 -0
  349. package/workflows/question-crops/scripts/image-ops.mjs +305 -0
  350. package/workflows/question-crops/scripts/image-ops.test.mjs +95 -0
  351. package/workflows/question-crops/workflow.mjs +172 -0
  352. package/workflows/ralph-loop/workflow.mjs +162 -0
  353. package/workflows/research/DESIGN.md +449 -0
  354. package/workflows/research/README.md +74 -0
  355. package/workflows/research/prompts/s0-goal.md +43 -0
  356. package/workflows/research/prompts/s1-early-exit.md +22 -0
  357. package/workflows/research/prompts/s1-map.md +58 -0
  358. package/workflows/research/prompts/s2-options.md +53 -0
  359. package/workflows/research/prompts/s3-deep-dive.md +56 -0
  360. package/workflows/research/prompts/s4-verify.md +74 -0
  361. package/workflows/research/prompts/s5-synthesis.md +81 -0
  362. package/workflows/research/prompts/s5b-fill-gaps.md +32 -0
  363. package/workflows/research/prompts/s6-package.md +32 -0
  364. package/workflows/research/scripts/build-report.mjs +98 -0
  365. package/workflows/research/workflow.mjs +288 -0
  366. package/workflows/wechat-reading-shots/prompts/collect-params.md +16 -0
  367. package/workflows/wechat-reading-shots/prompts/evaluate.md +20 -0
  368. package/workflows/wechat-reading-shots/prompts/open-book.md +28 -0
  369. package/workflows/wechat-reading-shots/prompts/read-page.md +13 -0
  370. package/workflows/wechat-reading-shots/prompts/turn-batch.md +24 -0
  371. package/workflows/wechat-reading-shots/test/control-flow.test.ts +329 -0
  372. package/workflows/wechat-reading-shots/workflow.mjs +266 -0
  373. package/workflows/wrong-question-docx/README.md +55 -0
  374. package/workflows/wrong-question-docx/prompts/figure-qa.md +8 -0
  375. package/workflows/wrong-question-docx/prompts/transcribe.md +25 -0
  376. package/workflows/wrong-question-docx/scripts/workbook.mjs +857 -0
  377. package/workflows/wrong-question-docx/test/control-flow.test.ts +165 -0
  378. package/workflows/wrong-question-docx/test/workbook.test.mjs +303 -0
  379. package/workflows/wrong-question-docx/workflow.mjs +485 -0
  380. package/plugins/workflow-runner/src/agentCommon.js +0 -354
  381. package/plugins/workflow-runner/src/agentCommon.ts +0 -435
  382. package/plugins/workflow-runner/src/controlFlow/branch.js +0 -40
  383. package/plugins/workflow-runner/src/controlFlow/branch.ts +0 -46
  384. package/plugins/workflow-runner/src/controlFlow/loop.js +0 -211
  385. package/plugins/workflow-runner/src/controlFlow/loop.ts +0 -248
  386. package/plugins/workflow-runner/src/controlFlow/pause.js +0 -129
  387. package/plugins/workflow-runner/src/controlFlow/pause.ts +0 -141
  388. package/plugins/workflow-runner/src/controlFlow/signal.js +0 -21
  389. package/plugins/workflow-runner/src/controlFlow/signal.ts +0 -29
  390. package/plugins/workflow-runner/src/controlFlow/types.js +0 -18
  391. package/plugins/workflow-runner/src/controlFlow/types.ts +0 -63
  392. package/plugins/workflow-runner/src/dslManifest.js +0 -72
  393. package/plugins/workflow-runner/src/dslManifest.ts +0 -127
  394. package/plugins/workflow-runner/src/expressions.js +0 -618
  395. package/plugins/workflow-runner/src/expressions.ts +0 -622
  396. package/plugins/workflow-runner/src/loader.js +0 -573
  397. package/plugins/workflow-runner/src/loader.ts +0 -721
  398. package/plugins/workflow-runner/src/multimodal.js +0 -38
  399. package/plugins/workflow-runner/src/multimodal.ts +0 -64
  400. package/plugins/workflow-runner/src/recovery.js +0 -119
  401. package/plugins/workflow-runner/src/recovery.ts +0 -149
  402. package/plugins/workflow-runner/src/stepExecutors/agent.js +0 -173
  403. package/plugins/workflow-runner/src/stepExecutors/agent.ts +0 -234
  404. package/plugins/workflow-runner/src/stepExecutors/assert.js +0 -41
  405. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -50
  406. package/plugins/workflow-runner/src/stepExecutors/echo.js +0 -31
  407. package/plugins/workflow-runner/src/stepExecutors/echo.ts +0 -38
  408. package/plugins/workflow-runner/src/stepExecutors/llm.js +0 -99
  409. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -133
  410. package/plugins/workflow-runner/src/stepExecutors/set.js +0 -49
  411. package/plugins/workflow-runner/src/stepExecutors/set.ts +0 -56
  412. package/plugins/workflow-runner/src/stepExecutors/skill.js +0 -111
  413. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -140
  414. package/plugins/workflow-runner/src/stepExecutors/tool.js +0 -208
  415. package/plugins/workflow-runner/src/stepExecutors/tool.ts +0 -256
  416. package/plugins/workflow-runner/src/stepExecutors/verify.js +0 -58
  417. package/plugins/workflow-runner/src/stepExecutors/verify.ts +0 -68
  418. package/plugins/workflow-runner/src/stepPipeline.js +0 -277
  419. package/plugins/workflow-runner/src/stepPipeline.ts +0 -366
  420. package/plugins/workflow-runner/test/agent.test.ts +0 -587
  421. package/plugins/workflow-runner/test/agentLlmError.test.ts +0 -373
  422. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +0 -65
  423. package/plugins/workflow-runner/test/captureMisbind.test.ts +0 -99
  424. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -147
  425. package/plugins/workflow-runner/test/context-files.test.ts +0 -320
  426. package/plugins/workflow-runner/test/e2e.test.ts +0 -370
  427. package/plugins/workflow-runner/test/engineRobustness.test.ts +0 -226
  428. package/plugins/workflow-runner/test/expressions.test.ts +0 -397
  429. package/plugins/workflow-runner/test/foundation.test.ts +0 -126
  430. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  431. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +0 -47
  432. package/plugins/workflow-runner/test/hostCapability.test.ts +0 -108
  433. package/plugins/workflow-runner/test/illustrate.load.test.ts +0 -183
  434. package/plugins/workflow-runner/test/jsonPayload.test.ts +0 -83
  435. package/plugins/workflow-runner/test/loader.test.ts +0 -617
  436. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +0 -33
  437. package/plugins/workflow-runner/test/multimodal.test.ts +0 -342
  438. package/plugins/workflow-runner/test/output-schema.test.ts +0 -213
  439. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +0 -87
  440. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -269
  441. package/plugins/workflow-runner/test/ppt.load.test.ts +0 -1220
  442. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +0 -220
  443. package/plugins/workflow-runner/test/recovery.test.ts +0 -265
  444. package/plugins/workflow-runner/test/runner.test.ts +0 -911
  445. package/plugins/workflow-runner/test/runnerPreflight.test.ts +0 -379
  446. package/plugins/workflow-runner/test/skillTurnDone.test.ts +0 -71
  447. package/plugins/workflow-runner/test/stateLifecycle.test.ts +0 -150
  448. package/plugins/workflow-runner/test/stepPipeline.test.ts +0 -114
  449. package/plugins/workflow-runner/test/taskArbitration.test.ts +0 -338
  450. package/plugins/workflow-runner/test/taskLifecycle.test.ts +0 -270
  451. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +0 -221
  452. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +0 -29
  453. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +0 -168
  454. package/plugins/workflow-runner/test/webppt.load.test.ts +0 -250
  455. package/plugins/workflow-runner/test/word.load.test.ts +0 -208
  456. package/plugins/workflow-runner/test/workflows.smoke.test.ts +0 -76
  457. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +0 -207
  458. package/src/advisorState.js +0 -22
  459. package/src/cli/configWizard.js +0 -293
  460. package/src/tools/toolProgressBridge.js +0 -48
  461. package/workflows/adaptive-research.yaml +0 -50
  462. package/workflows/book-review-short.yaml +0 -125
  463. package/workflows/e2e-write-greet.yaml +0 -31
  464. package/workflows/illustrate.yaml +0 -227
  465. package/workflows/plan.yaml +0 -470
  466. package/workflows/ppt.yaml +0 -1670
  467. package/workflows/ralph-loop.yaml +0 -199
  468. package/workflows/schema.json +0 -176
  469. package/workflows/webppt.yaml +0 -496
  470. package/workflows/wechat-reading-shots.yaml +0 -315
  471. package/workflows/word.yaml +0 -720
  472. package/workflows/youtube-shorts.yaml +0 -202
@@ -1,733 +1,783 @@
1
1
  /**
2
2
  * ============================================================
3
- * plugins/workflow-runner/src/workflowState.ts —— 工作流状态持久化
3
+ * plugins/workflow-runner/src/workflowState.ts —— 任务状态持久化
4
4
  * ------------------------------------------------------------
5
- * Phase 1 升级:支持断点续执行、任务身份判定、capture 回放、产物管理。
5
+ * 自 legacy 移植**持久化那一半**。legacy 版 1178 行里另一半是按「步骤索引 /
6
+ * 循环迭代 / VarStack 快照 / capture 变量」做的断点定位 —— 那些概念在代码
7
+ * 编排里不存在,由 `journal.ts` 的「调用序号 + 内容指纹」整体取代。
6
8
  *
7
- * 核心变更:
8
- * - init() 不再无条件清空,而是先读旧状态 → 判定任务身份 → 分发 4 种模式
9
- * - 新增 checkpoints/ 目录记录每步完成状态和 capture 变量
10
- * - 产物生命周期:current.json.artifacts 记录工作流【声明】的产物路径(相对 cwd,
11
- * 来自 yaml 的 def.artifacts / step.artifacts);new-task 时据此清旧产物(如旧 PLAN.md),
12
- * completed 时据此报实际存在的产物。不再用从不写入的 artifacts/ 子目录。
13
- * - resume 模式时回放已完成步骤的 capture 到 vars 中
14
- * - completed 检测:成功后 archive() 把目录改名 .done,故 init() 在活动目录缺失时
15
- * 回退读 .done/current.json(status=done + taskId 匹配 → completed),修活「completed 死代码」。
9
+ * 本文件负责的是**跨进程活得下来的东西**:
10
+ * - 目录布局与四态判定(fresh / new-task / completed / resume)
11
+ * - 声明产物的「先归档再清理」(含 glob 展开)
12
+ * - 用户补充要求(supplements)的持久化
13
+ * - 人类可读的 progress 日志
16
14
  *
17
- * 目录结构(E6:按 workflow 名隔离,跨工作流互不可见/互不干扰):
18
- * <cwd>/.minimal-agent-workflow/<name>/ (成功后改名为 <name>.done/)
19
- * ├── current.json # { name, taskId, status, startedAt, artifacts?, lastFailedStep?, lastError? }
20
- * ├── inputs.json # 用户传入的 inputs 快照
21
- * ├── vars.json # 最后一次成功 step 后的 VarStack.snapshot()
22
- * ├── progress.md # 时间戳追加的 step 完成日志
23
- * ├── supplements.json # 贯穿补充要求(pause 反馈/澄清结论,resume 恢复进锚定)
24
- * └── checkpoints/ # 每步一个 JSON:{ stepId, completedAt, output, captures, varsSnapshot }
25
- * # 顶层步用原名;顶层 loop 内步用复合名 loopId@@迭代号@@stepId
26
- * # (resume 可恢复到迭代中间,见 scanLoopResume)
27
- * /new 按 .minimal-agent 前缀清理父目录,行为不变。
15
+ * ------------------------------------------------------------
16
+ * ⚠️ **清理必须先归档,不能直接 rm** —— 这条是实战事故换来的:
17
+ * 同主题重跑时 LLM 仲裁误判成 new-task,11 个声明产物被静默直接删除、不可找回。
18
+ * 现在一律先 `rename` 进 `<实例目录>.trash/<stamp>/`(保留相对路径结构),
19
+ * rm 只作为 rename 失败的兜底。`.trash/` 是状态目录的 sibling,跨任务留存
20
+ * (淘汰策略暂未实现,需要时由用户手动清)。
21
+ *
22
+ * ⚠️ 归档失败时**绝不谎报归档位置** —— 一个文件都没成功移走就返回 undefined。
23
+ * 报了位置用户会去回收站找,找不到比一开始就说「已删除」更糟。
28
24
  * ============================================================
29
25
  */
30
26
 
31
- import { createHash } from 'node:crypto';
32
- import { mkdir, writeFile, appendFile, rm, rename, readFile, readdir } from 'node:fs/promises';
33
- import { existsSync } from 'node:fs';
34
- import { join } from 'node:path';
27
+ import { type Dirent, existsSync } from 'node:fs';
28
+ import {
29
+ appendFile,
30
+ mkdir,
31
+ readFile,
32
+ readdir,
33
+ rename,
34
+ rm,
35
+ writeFile,
36
+ } from 'node:fs/promises';
37
+ import { dirname, join, relative } from 'node:path';
38
+
35
39
  import {
40
+ type TaskArbitrator,
36
41
  computeTaskId,
37
42
  extractGoal,
38
- workflowStateDir,
39
- type TaskArbitrator,
43
+ sanitizeWorkflowDirName,
44
+ sessionStateDir,
45
+ taskInstanceDir,
40
46
  } from './taskIdentity.js';
41
- import type { StepDef } from './types.js';
47
+ import type { InputDef } from './types.js';
48
+ import { safeArtifactPath } from './util.js';
42
49
 
43
50
  const DONE_SUFFIX = '.done';
44
- const CHECKPOINTS_DIR = 'checkpoints';
45
- const SUPPLEMENTS_FILE = 'supplements.json';
46
-
47
- /**
48
- * 顶层 loop 内步的复合 checkpoint 文件名分隔符。
49
- * 选 '@@':yaml step id 习惯用 [a-z0-9_],不会撞;且对文件系统安全。
50
- */
51
- export const LOOP_CP_SEP = '@@';
52
-
53
- /** 复合 checkpoint 名:<loopId>@@<迭代号>@@<stepId>(runner 的 CpScope 与本模块扫描共用)。 */
54
- export function loopCheckpointName(loopId: string, iteration: number, stepId: string): string {
55
- return `${loopId}${LOOP_CP_SEP}${iteration}${LOOP_CP_SEP}${stepId}`;
56
- }
57
-
58
- /** 解析复合 checkpoint 文件名;非复合名(普通顶层步)返回 null。 */
59
- function parseLoopCheckpointName(
60
- name: string,
61
- ): { loopId: string; iteration: number; stepId: string } | null {
62
- const parts = name.split(LOOP_CP_SEP);
63
- if (parts.length !== 3) return null;
64
- const iteration = Number(parts[1]);
65
- if (!Number.isInteger(iteration) || iteration < 0) return null;
66
- return { loopId: parts[0], iteration, stepId: parts[2] };
67
- }
68
-
69
- /**
70
- * 工作流定义的**结构指纹**:步骤 id + 执行形态 + 嵌套块(then/else/do)骨架的 hash。
71
- * prompt 文案、args、condition 等内容字段不参与——改提示词/参数不应作废断点;
72
- * 改结构(增删步/换 id/换类型/调嵌套)才作废:resume 的恢复点是**索引**、循环断点是
73
- * 复合名(loopId@@迭代@@stepId),结构一变旧 checkpoint 就对不上,按旧断点续跑会错跳步
74
- * (曾真实发生:plan.yaml v3→v4 重构后同 goal 重跑提示「从第 2 步续跑」,跳过的步与
75
- * 新结构毫无对应关系)。current.json 记录运行时的指纹,init 同任务重跑时比对。
76
- */
77
- export function computeDefFingerprint(steps: StepDef[]): string {
78
- const skeleton = (list: StepDef[] | undefined): unknown[] =>
79
- (list ?? []).map((s) => ({
80
- id: s.id,
81
- kind:
82
- s.type ??
83
- (s.tool ? 'tool' : s.skill ? 'skill' : s.agent ? 'agent' : s.llm ? 'llm' : '?'),
84
- then: s.then ? skeleton(s.then) : undefined,
85
- else: s.else ? skeleton(s.else) : undefined,
86
- do: s.do ? skeleton(s.do) : undefined,
87
- }));
88
- return createHash('sha1')
89
- .update(JSON.stringify(skeleton(steps)))
90
- .digest('hex')
91
- .slice(0, 16);
92
- }
51
+ const TRASH_SUFFIX = '.trash';
93
52
 
94
- /** 进行中循环的恢复信息(buildResumeInfo 产出,runner 的 loop 处理器消费)。 */
95
- export interface LoopResumeInfo {
96
- /** 中断时正在执行的顶层 loop 步 id */
97
- loopId: string;
98
- /** 该 loop 在顶层 steps[] 中的索引(= resume 的顶层 startFrom) */
99
- loopIndex: number;
100
- /** 恢复到第几轮迭代(0-based,= 中断时的迭代号) */
101
- iteration: number;
102
- /** 该迭代内从第几个 do[] 步起跑(已含 backoff 回退) */
103
- innerStartFrom: number;
104
- /** 该迭代内已完成的 do[] 步 id(按 do[] 顺序) */
105
- completedInnerIds: string[];
106
- /** 中断时刻最近一个 checkpoint 的 vars 快照(runner 回放为全局变量,不含 inputs) */
107
- lastVarsSnapshot?: Record<string, unknown>;
108
- }
109
-
110
- // ---- 导出类型 ----
111
-
112
- /** 任务恢复模式 */
53
+ /** 四态:首次 / 换了任务 / 同任务已完成 / 同任务未完成可续跑。 */
113
54
  export type TaskResumeMode = 'fresh' | 'new-task' | 'completed' | 'resume';
114
55
 
115
- /** init() 返回的恢复信息 */
116
56
  export interface TaskResumeInfo {
117
57
  mode: TaskResumeMode;
118
- /** fresh/new-task/resume: 从第几个 step 索引开始执行 */
119
- startFrom?: number;
120
- /** new-task: 被清理的旧产物文件路径列表 */
58
+ /** fresh/new-task:旧产物归档到哪(相对 cwd)。归档一个都没成功时**不填**。 */
59
+ archivedTo?: string;
60
+ /** fresh/new-task:归档失败后被 rm 兜底删掉的路径。非空通常意味着归档出了问题。 */
121
61
  cleanedArtifacts?: string[];
122
62
  /**
123
- * new-task: 同任务但工作流定义结构已变更(或 legacy 状态无指纹无从验证)
124
- * → 旧断点作废、全新开始。runner 据此向用户明示原因(不是静默重头跑)。
63
+ * new-task:同一个 taskId,但**脚本内容变了**(指纹不匹配)→ 旧断点作废。
64
+ * 调用方据此向用户明示原因,而不是静默从头跑。
125
65
  */
126
- defChanged?: boolean;
127
- /** completed: 已完成的产物列表 */
66
+ scriptChanged?: boolean;
67
+ /** completed:磁盘上**实际存在**的产物(不是声明了就报)。 */
128
68
  artifacts?: string[];
129
- /** resume: 已完成的 step id 列表(按顺序) */
130
- completedStepIds?: string[];
131
- /** resume: 失败的 step id */
132
- failedStep?: string;
133
- /** resume: 自愈回退到的起始索引 */
134
- recoveryStart?: number;
135
- /** resume: 上次错误信息 */
69
+ /** resume:上次失败的原因。 */
136
70
  lastError?: string;
137
- /** resume: 已完成步骤的 capture 变量快照 { stepId: { captureKey: value } } */
138
- capturedVars?: Record<string, Record<string, unknown>>;
139
71
  /**
140
- * resume(仲裁判定的延续): 旧任务的原始 goal。runner 据此重建 goalAnchor——
141
- * 本次 inputs 里的 goal 是「继续」这类延续指令,不能拿它当锚(会让每步 LLM
142
- * 把「继续」当成目标);锚应是旧任务的真实目标。
72
+ * resume(仲裁判定的延续):旧任务的原始 goal。
73
+ *
74
+ * 本次输入是「继续」这类延续指令,**不能拿它当目标锚** —— 那会让后续每次
75
+ * 调用都把「继续」当成要达成的事。锚必须是旧任务的真实目标。
143
76
  */
144
77
  prevGoal?: string;
145
- /**
146
- * resume(仲裁判定的延续): 本次的新输入文本,作为旧目标的补充指令
147
- * 并入 goalAnchor(如「继续,但 PPT 改成 12 页」中的修改意见不丢失)。
148
- */
78
+ /** resume(仲裁判定的延续):本次新输入,作为旧目标的补充要求并入锚。 */
149
79
  supplementaryGoal?: string;
150
- /**
151
- * resume: 中断发生在某顶层 loop 的迭代中间时的恢复信息。
152
- * 存在时 startFrom = loopIndex(顶层直接定位到该 loop),迭代号与迭代内
153
- * 起点由 runner 的 loop 处理器消费;backoff 作用在迭代内而非顶层。
154
- */
155
- loopResume?: LoopResumeInfo;
156
- }
157
-
158
- // ---- 内部类型 ----
159
-
160
- /** 单个 checkpoint 的数据结构 */
161
- interface CheckpointData {
162
- stepId: string;
163
- completedAt: string; // ISO timestamp
164
- output: string; // step result preview
165
- captures: Record<string, unknown>; // 该步绑定的 capture 变量
166
- varsSnapshot: Record<string, unknown>; // 当时的全局 vars 快照
167
80
  }
168
81
 
169
- // ---- current.json 结构 ----
170
-
82
+ /** `current.json` 的形状。 */
171
83
  interface CurrentState {
172
84
  name: string;
173
85
  taskId: string;
174
- status: string;
86
+ status: 'running' | 'failed' | 'done';
175
87
  startedAt: string;
176
- lastFailedStep?: string;
88
+ goal: string;
177
89
  lastError?: string;
178
- /** 工作流声明的产物路径(相对 cwd),new-task 清理 + completed 报告用 */
90
+ /** 声明产物(相对 cwd):new-task 清理与 completed 报告用。 */
179
91
  artifacts?: string[];
180
- /** 运行时工作流定义的结构指纹(computeDefFingerprint),resume 兼容性比对用 */
181
- defFingerprint?: string;
92
+ /** Exact successful outputs, including owned descendants; declarations may include failed optional outputs. */
93
+ completion?: { artifacts: string[]; stateFiles: string[] };
94
+ /**
95
+ * 编排脚本全文的哈希。取代 legacy 的 `defFingerprint`(那是按步骤骨架算的)。
96
+ *
97
+ * 作用相同:同一个 taskId 但脚本改过 → 旧 journal 的调用序号对不上新脚本的
98
+ * 控制流 → 断点作废、明示原因、全新开始。legacy 曾因索引断点在 yaml 重构后
99
+ * 错跳步,被误读成 bug(真实发生过)。
100
+ */
101
+ scriptHash?: string;
102
+ }
103
+
104
+ /** 把 glob 段翻成正则片段(`*` 不跨目录分隔符,`?` 单字符)。 */
105
+ function artifactSegToPattern(seg: string): string {
106
+ return seg
107
+ .split(/([*?])/)
108
+ .map((part) => {
109
+ if (part === '*') return '[^/]*';
110
+ if (part === '?') return '[^/]';
111
+ return part.replace(/[.+^${}()|[\]\\]/g, '\\$&');
112
+ })
113
+ .join('');
182
114
  }
183
115
 
184
116
  export class WorkflowState {
185
- readonly dir: string;
117
+ /** 任务实例目录(绝对路径)。init 里确定 taskId 后才有值。 */
118
+ dir = '';
119
+ /** 当前 session 的目录(其下按 taskId 分实例)。 */
120
+ readonly sessionDir: string;
186
121
  readonly workingDir: string;
187
-
188
- constructor(workingDir: string, workflowName: string) {
122
+ readonly workflowName: string;
123
+ private readonly taskIdOverride?: string;
124
+ readonly sessionId: string;
125
+ /** init 里算出的任务身份。 */
126
+ taskId = '';
127
+ /**
128
+ * 输入声明。用于让 `extractGoal` 优先取作者标了 `required` 的那个输入 ——
129
+ * 任务身份与目标锚必须用**同一份**取值规则,否则「拿来算 taskId 的目标」
130
+ * 和「注入给模型的目标」会指向不同的东西。
131
+ */
132
+ private readonly defs?: InputDef[];
133
+ private progressWrites: Promise<void> = Promise.resolve();
134
+ private eventWrites: Promise<void> = Promise.resolve();
135
+
136
+ constructor(
137
+ workingDir: string,
138
+ workflowName: string,
139
+ sessionId: string,
140
+ defs?: InputDef[],
141
+ taskIdOverride?: string,
142
+ private readonly parentStateDir?: string,
143
+ ) {
189
144
  this.workingDir = workingDir;
190
- // 按 workflow 名隔离(与 taskIdentity.computeTaskId 读的是同一权威路径)
191
- this.dir = workflowStateDir(workingDir, workflowName);
145
+ this.workflowName = workflowName;
146
+ this.sessionId = sanitizeWorkflowDirName(sessionId);
147
+ this.sessionDir = parentStateDir
148
+ ? join(parentStateDir, 'children', workflowName)
149
+ : sessionStateDir(workingDir, workflowName, sessionId);
150
+ this.defs = defs;
151
+ this.taskIdOverride = taskIdOverride;
152
+ }
153
+
154
+ /** journal 落盘位置(由 journal.ts 消费,见任务 5)。 */
155
+ get journalPath(): string {
156
+ return join(this.dir, 'journal.jsonl');
157
+ }
158
+
159
+ private get currentPath(): string {
160
+ return join(this.dir, 'current.json');
161
+ }
162
+
163
+ private get supplementsPath(): string {
164
+ return join(this.dir, 'supplements.json');
165
+ }
166
+
167
+ private get progressPath(): string {
168
+ return join(this.dir, 'progress.md');
169
+ }
170
+
171
+ private get eventsPath(): string {
172
+ return join(this.dir, 'events.jsonl');
192
173
  }
193
174
 
194
175
  /**
195
- * 初始化工作流状态 —— 核心入口,返回恢复信息。
176
+ * 判定本次运行属于四态里的哪一种,并把实例目录准备好。
196
177
  *
197
- * 4 种模式:
198
- * 1. fresh — 首次运行此工作流(无旧状态)
199
- * 2. new-task — 不同任务 → 清理旧产物后全新开始
200
- * 3. completed — 同一任务已完成 → 返回产物列表
201
- * 4. resume — 同一任务未完成 → 扫描 checkpoint 决定恢复点
202
- *
203
- * hash 相等 = 同任务(权威信号,与时间无关)。hash 不等且旧任务【未完成】时
204
- * 不再一票否决 new-task:若提供了 arbitrate(语义仲裁器,见 taskIdentity.ts),
205
- * 由它判定新输入是「旧任务的延续指令(如"继续")→ resume」还是「全新任务 → new-task」。
178
+ * @param inputs 本次调用的输入(用于算 taskId 与 goal)
179
+ * @param scriptHash 编排脚本全文哈希(脚本改了 → 断点作废)
180
+ * @param declaredArtifacts meta.artifacts 种子(运行时还会经 declareArtifact 增长)
181
+ * @param arbitrate 可选的语义仲裁器;不传则 hash 不等一律 new-task
206
182
  */
207
183
  async init(
208
- name: string,
209
184
  inputs: Record<string, unknown>,
210
- steps: StepDef[],
211
- // 跨次重跑回退步数(= WorkflowDef.self_heal.backoff),runWorkflow 注入;缺省 1。
212
- selfHealBackoff = 1,
213
- // 工作流【声明】的产物路径(相对 cwd,= def.artifacts + step.artifacts),runWorkflow 注入。
214
- declaredArtifacts: string[] = [],
215
- // 语义仲裁器(hash 不等 + 旧任务未完成时的二审),runWorkflow 注入;缺省直接 new-task。
185
+ scriptHash: string,
186
+ declaredArtifacts: string[],
216
187
  arbitrate?: TaskArbitrator,
217
188
  ): Promise<TaskResumeInfo> {
218
- // 1. 计算当前任务的 identity(goal 内容 hash 是唯一权威信号)
219
- // 与本次定义的结构指纹(resume 兼容性信号:yaml 结构变了旧断点即作废)
220
- const identity = computeTaskId(name, inputs, this.workingDir);
221
- const fp = computeDefFingerprint(steps);
222
-
223
- // 2. 尝试读取活动目录的 current.json
224
- const oldState = await this.readCurrentState();
225
-
226
- if (!oldState) {
227
- // 活动目录无状态 —— 可能是「上次成功已归档到 .done」。读归档判定 completed / 不同 goal 新任务。
228
- const archived = await this.readArchivedState();
229
- if (archived && archived.name === name) {
230
- if (archived.taskId === identity.hash) {
231
- // 模式 3: completed —— 同任务此前已成功(报实际存在的声明产物)
232
- return {
233
- mode: 'completed',
234
- artifacts: this.existingArtifacts(archived.artifacts ?? declaredArtifacts),
235
- };
236
- }
237
- // 同名 workflow 但 goal 不同 → 全新任务:清旧归档声明产物后 fresh
238
- // (legacy 归档无 artifacts 字段时用本次声明清单兜底——同名 workflow 的声明产物
239
- // 语义稳定,旧任务的产物路径与本次一致的概率极高,残留比误删危害大)
240
- const cleaned = await this.cleanupDeclaredArtifacts(
241
- archived.artifacts ?? declaredArtifacts,
189
+ const goal = extractGoal(inputs, this.defs);
190
+ this.taskId = this.taskIdOverride
191
+ ? sanitizeWorkflowDirName(this.taskIdOverride)
192
+ : computeTaskId(this.workflowName, inputs, this.defs);
193
+ const taskDir = this.parentStateDir
194
+ ? join(this.sessionDir, this.taskId)
195
+ : taskInstanceDir(
196
+ this.workingDir,
197
+ this.workflowName,
198
+ this.sessionId,
199
+ this.taskId,
242
200
  );
243
- await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
244
- return {
245
- mode: 'new-task',
246
- startFrom: 0,
247
- cleanedArtifacts: [
248
- ...cleaned,
249
- ...(await this.cleanupStaleDeclared(declaredArtifacts)),
250
- ],
251
- };
201
+ const doneDir = taskDir + DONE_SUFFIX;
202
+
203
+ // ① 同任务、已完成 → 报实际存在的产物,不重跑。
204
+ if (existsSync(doneDir)) {
205
+ this.dir = doneDir;
206
+ const prev = await this.readCurrent();
207
+ // ⚠️ **脚本改过就不算已完成**。上次那份结论是**另一份脚本**产出的 ——
208
+ // 作者刚改完工作流重跑同一个目标,最想要的恰恰是「按新逻辑再来一遍」,
209
+ // 这时回一句「此前已完成」并甩出旧产物,是**静默给错答案**。
210
+ // (这条是端到端实跑抓到的:改完脚本重跑,1 秒返回旧结果。)
211
+ if (
212
+ prev &&
213
+ ((prev.scriptHash && prev.scriptHash !== scriptHash) ||
214
+ !(await this.treeIntact()))
215
+ ) {
216
+ const info = await this.restartChanged(
217
+ goal,
218
+ scriptHash,
219
+ declaredArtifacts,
220
+ prev,
221
+ taskDir,
222
+ );
223
+ return { ...info, mode: 'new-task', scriptChanged: true };
252
224
  }
253
- // 模式 1: fresh —— 活动 + 归档都没有,首次运行。
254
- // 声明产物若已存在于磁盘 = 残留孤儿(如 /new 删状态目录后的旧 PLAN.md,
255
- // artifacts 清单随 current.json 一起被删,引擎无从知道它属于谁)。
256
- // 用户约定:全新任务开始时清理残留与中间文件——删除范围**仅限本工作流
257
- // 声明的产物**(与运行自身对它们的覆盖写权限一致,不碰用户其他文件)。
258
- await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
259
- return {
260
- mode: 'fresh',
261
- startFrom: 0,
262
- cleanedArtifacts: await this.cleanupStaleDeclared(declaredArtifacts),
263
- };
225
+ const artifacts = await this.allArtifacts();
226
+ return { mode: 'completed', artifacts };
264
227
  }
265
228
 
266
- // 3. 有活动状态(上次中断 / 失败 / 进行中,未归档)—— 用 hash 判同/异任务
267
- if (!identity.isSameTask) {
268
- // hash 不等 + 同名 workflow + 旧任务未完成 → 语义仲裁二审:
269
- // 新输入可能是「继续」这类延续指令(goal 文本必然与旧任务不同,按旧规则
270
- // 会被误判 new-task 把没做完的产物清掉重开)。不同名 workflow / 旧任务已
271
- // 完成 / 未提供仲裁器 → 维持 new-task。仲裁器自身抛错也兜底 new-task。
229
+ // ② 同任务、未完成 → resume(能续多远由 journal 说了算)。
230
+ if (existsSync(taskDir)) {
231
+ this.dir = taskDir;
232
+ const prev = await this.readCurrent();
272
233
  if (
273
- arbitrate &&
274
- identity.reason === 'hash_diff' &&
275
- oldState.name === name &&
276
- oldState.status !== 'done' &&
277
- // 定义结构已变(或 legacy 无指纹)→ 旧断点不可续,仲裁 resume 无意义,
278
- // 跳过仲裁直接走 new-task
279
- oldState.defFingerprint === fp
234
+ prev &&
235
+ ((prev.scriptHash && prev.scriptHash !== scriptHash) ||
236
+ !(await this.treeIntact()))
280
237
  ) {
281
- const prevGoal = extractGoal((await this.readInputs()) ?? {});
282
- const newGoal = extractGoal(inputs);
283
- let verdict: 'resume' | 'new' = 'new';
284
- try {
285
- verdict = await arbitrate({
286
- workflowName: name,
287
- prevGoal,
288
- newGoal,
289
- completedStepIds: await this.scanCheckpoints(steps),
290
- totalSteps: steps.length,
291
- artifacts: oldState.artifacts ?? [],
292
- });
293
- } catch {
294
- verdict = 'new';
295
- }
296
- if (verdict === 'resume') {
297
- // 延续旧任务:状态目录原样保留(taskId 仍是旧 goal 的 hash——
298
- // 下次再输「继续」依然 hash 不等 → 再次仲裁快路径命中,语义自洽)。
299
- const info = await this.buildResumeInfo(oldState, steps, selfHealBackoff);
300
- info.prevGoal = prevGoal;
301
- info.supplementaryGoal = newGoal;
302
- return info;
303
- }
238
+ // 脚本结构变了:旧 journal 的调用序号对不上新控制流,断点必须作废。
239
+ const info = await this.restartChanged(
240
+ goal,
241
+ scriptHash,
242
+ declaredArtifacts,
243
+ prev,
244
+ taskDir,
245
+ );
246
+ return { ...info, mode: 'new-task', scriptChanged: true };
304
247
  }
305
-
306
- // 模式 2: new-task —— 不同任务,清旧声明产物 + 旧状态后全新开始。
307
- // legacy 状态文件无 artifacts 字段时:仅当同名 workflow 用本次声明清单兜底
308
- //(不同名 workflow 的产物不是我们的,不能殃及)。
309
- const cleaned = await this.cleanupDeclaredArtifacts(
310
- oldState.artifacts ?? (oldState.name === name ? declaredArtifacts : []),
311
- );
312
- await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
313
- return {
314
- mode: 'new-task',
315
- startFrom: 0,
316
- cleanedArtifacts: [
317
- ...cleaned,
318
- ...(await this.cleanupStaleDeclared(declaredArtifacts)),
248
+ await this.writeCurrent({
249
+ name: this.workflowName,
250
+ taskId: this.taskId,
251
+ status: 'running',
252
+ startedAt: new Date().toISOString(),
253
+ goal: prev?.goal ?? goal,
254
+ artifacts: [
255
+ ...new Set([...(prev?.artifacts ?? []), ...declaredArtifacts]),
319
256
  ],
320
- };
321
- }
322
-
323
- // 4. 同一任务 —— 检查完成状态(兜底:正常成功会 archive,故活动目录 done 少见)
324
- if (oldState.status === 'done') {
325
- return {
326
- mode: 'completed',
327
- artifacts: this.existingArtifacts(oldState.artifacts ?? declaredArtifacts),
328
- };
257
+ scriptHash,
258
+ });
259
+ return { mode: 'resume', lastError: prev?.lastError };
329
260
  }
330
261
 
331
- // 4.5 定义结构变更守卫:同任务但 yaml 结构改过(增删步/换 id/调嵌套)——
332
- // resume 恢复点是索引、循环断点是复合名,结构一变旧 checkpoint 全对不上,
333
- // 按旧断点续跑会错跳步(曾真实发生:plan v3→v4 重构后同 goal 重跑
334
- // 「从第 2 步续跑」,用户困惑是 bug 还是残留)。旧现场作废、全新开始,
335
- // runner 经 defChanged 旗标向用户明示。legacy 状态(无 defFingerprint
336
- // 字段)无从验证兼容性,同样按变更处理(一次性升级成本,安全优先)。
337
- if (oldState.defFingerprint !== fp) {
338
- const cleaned = await this.cleanupDeclaredArtifacts(
339
- oldState.artifacts ?? declaredArtifacts,
340
- );
341
- await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
342
- return {
343
- mode: 'new-task',
344
- startFrom: 0,
345
- defChanged: true,
346
- cleanedArtifacts: [
347
- ...cleaned,
348
- ...(await this.cleanupStaleDeclared(declaredArtifacts)),
349
- ],
350
- };
262
+ // ③ 本 taskId 没有目录。看看同 session 下有没有**未完成的兄弟任务**可以仲裁延续。
263
+ const sibling = await this.findIncompleteSibling();
264
+ // Bound resources and their declared parameters are an exact contract.
265
+ // A different task hash must not be overridden by a semantic guess based
266
+ // only on the (possibly identical) human-readable goal.
267
+ if (sibling && arbitrate && !inputs.__resource_fingerprint) {
268
+ const prev = await this.readCurrentAt(sibling);
269
+ const verdict = await arbitrate({
270
+ workflowName: this.workflowName,
271
+ prevGoal: prev?.goal ?? '',
272
+ newGoal: goal,
273
+ completedCalls: await this.countJournalEntries(sibling),
274
+ artifacts: await this.existingArtifacts(prev?.artifacts ?? []),
275
+ });
276
+ if (verdict === 'resume') {
277
+ // 采纳旧实例目录继续跑:本次输入降级成补充要求,目标锚仍指向旧目标。
278
+ this.dir = sibling;
279
+ this.taskId = prev?.taskId ?? this.taskId;
280
+ return {
281
+ mode: 'resume',
282
+ prevGoal: prev?.goal,
283
+ supplementaryGoal: goal,
284
+ lastError: prev?.lastError,
285
+ };
286
+ }
351
287
  }
352
288
 
353
- // 5. 模式 4: resume —— 同一任务未完成,扫描 checkpoint 决定恢复点。
354
- return this.buildResumeInfo(oldState, steps, selfHealBackoff);
289
+ // ④ 全新任务(或首次运行)。
290
+ this.dir = taskDir;
291
+ const info = await this.startFresh(goal, scriptHash, declaredArtifacts);
292
+ return { ...info, mode: sibling ? 'new-task' : 'fresh' };
355
293
  }
356
294
 
357
- /**
358
- * 构建 resume 信息:扫描 checkpoint 决定恢复点 + 加载 capture 回放数据。
359
- * 回退步数由 WorkflowDef.self_heal.backoff 经 runWorkflow 传入(缺省 1)。
360
- * 同 goal resume(hash 相等)与仲裁延续 resume(hash 不等但判定延续)共用。
295
+ /** 建实例目录、归档并清理旧产物残留、写 current.json。 */
296
+ /** Retire the actual old journal AND dynamic outputs, not only the status flag.
297
+ * A completed directory must also be retired, otherwise it shadows the fresh
298
+ * incomplete attempt on the next resume. Nothing is deleted on this path.
361
299
  */
362
- private async buildResumeInfo(
363
- oldState: CurrentState,
364
- steps: StepDef[],
365
- selfHealBackoff: number,
300
+ private async restartChanged(
301
+ goal: string,
302
+ scriptHash: string,
303
+ declaredArtifacts: string[],
304
+ previous: CurrentState,
305
+ taskDir: string,
366
306
  ): Promise<TaskResumeInfo> {
367
- const completedSteps = await this.scanCheckpoints(steps);
368
-
369
- // 恢复点 = 最后一个已完成步在 steps[] 中的【索引】+1 - backoff。
370
- // 不能用 checkpoint【计数】当索引:when=false 跳过步 / pause 步不记 checkpoint,
371
- // 计数会低估位置 → resume 重复执行已跑过的步(对非幂等 tool 步有实害)。
372
- // 已完成步是连续前缀时索引法与计数法等价(向后兼容)。
373
- const lastCompletedId = completedSteps[completedSteps.length - 1];
374
- const lastIdx =
375
- lastCompletedId !== undefined
376
- ? steps.findIndex((s) => s.id === lastCompletedId)
377
- : -1;
378
- const recoveryStart = Math.max(0, lastIdx + 1 - selfHealBackoff);
379
-
380
- // 加载 capturedVars 用于回放
381
- const capturedVars = await this.loadAllCaptures(completedSteps);
382
-
383
- // 循环级恢复:中断发生在某顶层 loop 的迭代中间(该 loop 自身无完成 checkpoint、
384
- // 但有复合迭代 checkpoint)→ 顶层直接定位到该 loop、迭代内续跑,
385
- // 不再整条循环从头重来(plan.yaml 95% 的工作都在一个 loop 里,此前粒度形同虚设)。
386
- const loopResume = await this.scanLoopResume(steps, completedSteps, selfHealBackoff);
387
- if (loopResume) {
388
- return {
389
- mode: 'resume',
390
- startFrom: loopResume.loopIndex,
391
- completedStepIds: completedSteps,
392
- failedStep: oldState.lastFailedStep,
393
- recoveryStart: loopResume.loopIndex,
394
- lastError: oldState.lastError,
395
- capturedVars,
396
- loopResume,
397
- };
307
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
308
+ const archiveRoot = join(this.dir + TRASH_SUFFIX, stamp);
309
+ const existing = [
310
+ ...new Set([
311
+ ...(await this.allArtifacts()),
312
+ ...(await this.existingArtifacts(declaredArtifacts)),
313
+ ]),
314
+ ];
315
+ await this.archiveDeclaredArtifacts(existing, stamp);
316
+ const remaining = existing.filter((path) =>
317
+ existsSync(join(this.workingDir, path)),
318
+ );
319
+ if (remaining.length)
320
+ throw new Error(`旧产物归档失败,停止覆盖:${remaining.join('、')}`);
321
+ await mkdir(archiveRoot, { recursive: true });
322
+ const oldDir = this.dir;
323
+ await rename(oldDir, join(archiveRoot, 'state'));
324
+ // Older versions could leave both .done and an incomplete directory.
325
+ if (oldDir !== taskDir && existsSync(taskDir)) {
326
+ await rename(taskDir, join(archiveRoot, 'incomplete-state'));
398
327
  }
328
+ this.dir = taskDir;
329
+ const info = await this.startFresh(goal, scriptHash, declaredArtifacts);
330
+ return { ...info, archivedTo: relative(this.workingDir, archiveRoot) };
331
+ }
399
332
 
400
- return {
401
- mode: 'resume',
402
- startFrom: recoveryStart,
403
- completedStepIds: completedSteps,
404
- failedStep: oldState.lastFailedStep,
405
- recoveryStart,
406
- lastError: oldState.lastError,
407
- capturedVars,
408
- };
333
+ private async startFresh(
334
+ goal: string,
335
+ scriptHash: string,
336
+ declaredArtifacts: string[],
337
+ ): Promise<TaskResumeInfo> {
338
+ // 时间戳只用作回收目录名,不参与任何身份判定 —— 故这里用真实时间是安全的。
339
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
340
+ const { cleaned, archivedTo } = await this.archiveThenClean(
341
+ declaredArtifacts,
342
+ stamp,
343
+ );
344
+ await mkdir(this.dir, { recursive: true });
345
+ await writeFile(this.supplementsPath, '[]', 'utf8');
346
+ await this.writeCurrent({
347
+ name: this.workflowName,
348
+ taskId: this.taskId,
349
+ status: 'running',
350
+ startedAt: new Date().toISOString(),
351
+ goal,
352
+ artifacts: declaredArtifacts,
353
+ scriptHash,
354
+ });
355
+ const info: TaskResumeInfo = { mode: 'fresh' };
356
+ if (cleaned.length > 0) info.cleanedArtifacts = cleaned;
357
+ if (archivedTo) info.archivedTo = archivedTo;
358
+ return info;
409
359
  }
410
360
 
411
- /**
412
- * 扫描复合迭代 checkpoint(loopId@@iter@@stepId),产出进行中循环的恢复信息。
413
- * 候选条件:顶层 type=loop 步 + 自身无完成 checkpoint(completedTopIds 不含它)。
414
- * 多个候选(罕见,前一循环中断后又跑到后一循环)取顶层索引最大者(最远进度)。
415
- */
416
- private async scanLoopResume(
417
- steps: StepDef[],
418
- completedTopIds: string[],
419
- selfHealBackoff: number,
420
- ): Promise<LoopResumeInfo | null> {
421
- const cpDir = join(this.dir, CHECKPOINTS_DIR);
422
- let files: string[];
361
+ /** 同 session 下另一个**未完成**任务的目录(有多个时取最近修改的)。 */
362
+ private async findIncompleteSibling(): Promise<string | null> {
363
+ let entries: Dirent[];
423
364
  try {
424
- files = await readdir(cpDir);
365
+ entries = await readdir(this.sessionDir, { withFileTypes: true });
425
366
  } catch {
426
367
  return null;
427
368
  }
428
- const completedTop = new Set(completedTopIds);
429
- // loopId → iteration → stepId 集合
430
- const byLoop = new Map<string, Map<number, Set<string>>>();
431
- for (const f of files) {
432
- if (!f.endsWith('.json')) continue;
433
- const parsed = parseLoopCheckpointName(f.replace(/\.json$/, ''));
434
- if (!parsed) continue;
435
- let iters = byLoop.get(parsed.loopId);
436
- if (!iters) byLoop.set(parsed.loopId, (iters = new Map()));
437
- let ids = iters.get(parsed.iteration);
438
- if (!ids) iters.set(parsed.iteration, (ids = new Set()));
439
- ids.add(parsed.stepId);
440
- }
441
- if (byLoop.size === 0) return null;
442
-
443
- let best: LoopResumeInfo | null = null;
444
- for (const [loopId, iters] of byLoop) {
445
- const loopIndex = steps.findIndex((s) => s.id === loopId && s.type === 'loop');
446
- if (loopIndex === -1) continue; // yaml 改动后的孤儿 checkpoint,忽略
447
- if (completedTop.has(loopId)) continue; // 该循环已整体完成
448
- const loopDef = steps[loopIndex];
449
- const doSteps = loopDef.do ?? [];
450
- const validInner = new Set(doSteps.map((s) => s.id));
451
-
452
- const iteration = Math.max(...iters.keys());
453
- const innerIds = [...(iters.get(iteration) ?? [])]
454
- .filter((id) => validInner.has(id))
455
- .sort(
456
- (a, b) =>
457
- doSteps.findIndex((s) => s.id === a) - doSteps.findIndex((s) => s.id === b),
458
- );
459
- if (innerIds.length === 0) continue;
460
-
461
- const lastInnerIdx = doSteps.findIndex(
462
- (s) => s.id === innerIds[innerIds.length - 1],
463
- );
464
- const innerStartFrom = Math.max(0, lastInnerIdx + 1 - selfHealBackoff);
465
-
466
- // 中断时刻最近的 vars 快照(迭代内最后一个完成步的 checkpoint)
467
- let lastVarsSnapshot: Record<string, unknown> | undefined;
468
- try {
469
- const raw = await readFile(
470
- join(cpDir, `${loopCheckpointName(loopId, iteration, innerIds[innerIds.length - 1])}.json`),
471
- 'utf-8',
472
- );
473
- lastVarsSnapshot = (JSON.parse(raw) as CheckpointData).varsSnapshot;
474
- } catch {
475
- // 快照读失败不阻断:仍可定位迭代,变量靠顶层 capture 回放兜底
369
+ const candidates = entries
370
+ .filter(
371
+ (e) =>
372
+ e.isDirectory() &&
373
+ !e.name.endsWith(DONE_SUFFIX) &&
374
+ !e.name.endsWith(TRASH_SUFFIX) &&
375
+ e.name !== this.taskId,
376
+ )
377
+ .map((e) => join(this.sessionDir, e.name))
378
+ .filter((d) => existsSync(join(d, 'current.json')));
379
+ if (candidates.length === 0) return null;
380
+ // 多个未完成实例时取最近的那个 —— 用户嘴里的「继续」几乎总是指最近一次。
381
+ let newest = candidates[0];
382
+ let newestAt = '';
383
+ for (const dir of candidates) {
384
+ const cur = await this.readCurrentAt(dir);
385
+ const at = cur?.startedAt ?? '';
386
+ if (at >= newestAt) {
387
+ newestAt = at;
388
+ newest = dir;
476
389
  }
390
+ }
391
+ return newest;
392
+ }
477
393
 
478
- const candidate: LoopResumeInfo = {
479
- loopId,
480
- loopIndex,
481
- iteration,
482
- innerStartFrom,
483
- completedInnerIds: innerIds,
484
- lastVarsSnapshot,
485
- };
486
- if (!best || candidate.loopIndex > best.loopIndex) best = candidate;
394
+ private async countJournalEntries(dir: string): Promise<number> {
395
+ try {
396
+ const raw = await readFile(join(dir, 'journal.jsonl'), 'utf8');
397
+ return raw.split('\n').filter((l) => l.trim() !== '').length;
398
+ } catch {
399
+ return 0;
487
400
  }
488
- return best;
489
401
  }
490
402
 
491
- // ---- 公共方法:贯穿补充要求持久化(goalSupplements,runner 写/读) ----
403
+ // ---- current.json ----
492
404
 
493
- /** 追加一条用户补充要求到 supplements.json(resume 跨进程不丢)。 */
494
- async appendSupplement(text: string): Promise<void> {
495
- const list = await this.readSupplements();
496
- list.push(text);
497
- await writeFile(
498
- join(this.dir, SUPPLEMENTS_FILE),
499
- JSON.stringify(list, null, 2),
500
- 'utf-8',
501
- ).catch(() => {});
405
+ private async readCurrent(): Promise<CurrentState | null> {
406
+ return this.readCurrentAt(this.dir);
502
407
  }
503
408
 
504
- /** 读取已累积的补充要求列表(无文件 / 解析失败返回 [])。 */
505
- async readSupplements(): Promise<string[]> {
409
+ private async readCurrentAt(dir: string): Promise<CurrentState | null> {
506
410
  try {
507
- const raw = await readFile(join(this.dir, SUPPLEMENTS_FILE), 'utf-8');
508
- const parsed = JSON.parse(raw);
509
- return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : [];
411
+ return JSON.parse(
412
+ await readFile(join(dir, 'current.json'), 'utf8'),
413
+ ) as CurrentState;
510
414
  } catch {
511
- return [];
415
+ return null;
512
416
  }
513
417
  }
514
418
 
515
- // ---- 公共方法:checkpoint / 失败 / 完成 ----
516
-
517
- /** 记录单个步骤成功完成(含 capture 变量和 vars 快照) */
518
- async recordCheckpoint(
519
- stepId: string,
520
- output: string,
521
- captures: Record<string, unknown>,
522
- varsSnapshot: Record<string, unknown>,
523
- ): Promise<void> {
524
- const cpDir = join(this.dir, CHECKPOINTS_DIR);
525
- await mkdir(cpDir, { recursive: true }).catch(() => {});
526
-
527
- const data: CheckpointData = {
528
- stepId,
529
- completedAt: new Date().toISOString(),
530
- output,
531
- captures,
532
- varsSnapshot,
533
- };
419
+ private async writeCurrent(state: CurrentState): Promise<void> {
420
+ await mkdir(this.dir, { recursive: true });
421
+ await writeFile(this.currentPath, JSON.stringify(state, null, 2), 'utf8');
422
+ }
534
423
 
535
- await writeFile(join(cpDir, `${stepId}.json`), JSON.stringify(data, null, 2), 'utf-8');
424
+ /** 记一次失败原因(下次 resume 时报给用户 / 喂给模型)。 */
425
+ async recordFailure(error: string, artifacts?: string[]): Promise<void> {
426
+ const cur = await this.readCurrent();
427
+ if (!cur) return;
428
+ await this.writeCurrent({
429
+ ...cur,
430
+ status: 'failed',
431
+ lastError: error,
432
+ ...(artifacts ? { artifacts: [...new Set(artifacts)] } : {}),
433
+ });
536
434
  }
537
435
 
538
- /** 记录步骤失败:更新 current.json 的 lastFailedStep 和 lastError */
539
- async recordFailure(stepId: string, error: string): Promise<void> {
540
- const current = await this.readCurrentState();
541
- if (current) {
542
- current.lastFailedStep = stepId;
543
- current.lastError = error;
544
- current.status = 'failed';
545
- await this.writeCurrent(current);
546
- }
436
+ /**
437
+ * 标记完成(随后 archive() 把目录改名成 `.done`)。
438
+ *
439
+ * @param artifacts 本次运行**最终**的声明产物集合。必须传 —— `meta.artifacts`
440
+ * 只是静态种子,循环体里经 `declareArtifact()` 登记的逐迭代产物只活在内存里。
441
+ * 不在这里落盘的话,下次 completed 判定会报「本次运行没有声明产物」,
442
+ * new-task 清理也会漏掉它们 —— 而且**表现为静默漏档而不是报错**。
443
+ * (这条是端到端实跑抓到的:上一轮明明 declareArtifact 了,重跑却报 0 产物。)
444
+ */
445
+ async markDone(artifacts?: string[]): Promise<void> {
446
+ const cur = await this.readCurrent();
447
+ if (!cur) return;
448
+ if (!existsSync(join(this.dir, 'return.json')))
449
+ await this.writeResult(null);
450
+ const owned = artifacts ?? cur.artifacts ?? [];
451
+ const records = await this.treeRecords();
452
+ const treeArtifacts = [
453
+ ...owned,
454
+ ...records
455
+ .filter((r) => r.dir !== this.dir)
456
+ .flatMap((r) => r.state.artifacts ?? []),
457
+ ];
458
+ await this.writeCurrent({
459
+ ...cur,
460
+ status: 'done',
461
+ ...(artifacts ? { artifacts: [...new Set(artifacts)] } : {}),
462
+ completion: {
463
+ artifacts: await this.existingArtifacts(treeArtifacts),
464
+ stateFiles: [
465
+ 'return.json',
466
+ ...records
467
+ .filter((r) => r.dir !== this.dir)
468
+ .map((r) => relative(this.dir, join(r.dir, 'current.json'))),
469
+ ],
470
+ },
471
+ });
547
472
  }
548
473
 
549
- /** 标记整个工作流完成 */
550
- async markDone(): Promise<void> {
551
- const current = await this.readCurrentState();
552
- if (current) {
553
- current.status = 'done';
554
- await this.writeCurrent(current);
474
+ async writeResult(value: unknown): Promise<unknown> {
475
+ const json = JSON.stringify(value ?? null, (_key, v) => {
476
+ if (
477
+ typeof v === 'function' ||
478
+ typeof v === 'symbol' ||
479
+ typeof v === 'bigint' ||
480
+ (typeof v === 'number' && !Number.isFinite(v))
481
+ )
482
+ throw new Error('工作流返回值必须为JSON数据');
483
+ return v;
484
+ });
485
+ await writeFile(join(this.dir, 'return.json'), json, 'utf8');
486
+ return JSON.parse(json);
487
+ }
488
+
489
+ async readResult(): Promise<unknown> {
490
+ try {
491
+ return JSON.parse(await readFile(join(this.dir, 'return.json'), 'utf8'));
492
+ } catch (error) {
493
+ if (!(await this.readCurrent())?.completion) return null; // pre-composition state
494
+ throw error;
555
495
  }
556
496
  }
557
497
 
558
- // ---- 公共方法:进度与变量(保持原有签名兼容) ----
498
+ /** Children live inside the parent's state directory. Moving the parent also
499
+ * archives all child journals, never another standalone run of the same name. */
500
+ private async treeRecords(): Promise<{ dir: string; state: CurrentState }[]> {
501
+ const records: { dir: string; state: CurrentState }[] = [];
502
+ const own = await this.readCurrent();
503
+ if (own) records.push({ dir: this.dir, state: own });
504
+ const visit = async (dir: string, depth: number): Promise<void> => {
505
+ if (depth > 24) throw new Error('子工作流状态目录嵌套异常');
506
+ let entries: Dirent[];
507
+ try {
508
+ entries = await readdir(dir, { withFileTypes: true });
509
+ } catch (error) {
510
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
511
+ throw error;
512
+ }
513
+ for (const e of entries) {
514
+ if (!e.isDirectory() || e.name.endsWith(TRASH_SUFFIX)) continue;
515
+ const childDir = join(dir, e.name);
516
+ const current = await this.readCurrentAt(childDir);
517
+ if (current) {
518
+ records.push({ dir: childDir, state: current });
519
+ await visit(join(childDir, 'children'), depth + 1);
520
+ } else await visit(childDir, depth + 1);
521
+ }
522
+ };
523
+ await visit(join(this.dir, 'children'), 0);
524
+ return records;
525
+ }
559
526
 
560
- /** 追加进度日志 */
561
- async appendProgress(line: string): Promise<void> {
562
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
563
- await appendFile(join(this.dir, 'progress.md'), `- ${ts} ${line}\n`, 'utf-8').catch(() => {});
527
+ async allArtifacts(): Promise<string[]> {
528
+ return this.existingArtifacts(
529
+ (await this.treeRecords()).flatMap((r) => r.state.artifacts ?? []),
530
+ );
564
531
  }
565
532
 
566
- /** 写入 vars 快照 */
567
- async writeVars(snapshot: Record<string, unknown>): Promise<void> {
568
- await writeFile(join(this.dir, 'vars.json'), JSON.stringify(snapshot, null, 2), 'utf-8').catch(() => {});
533
+ private async treeIntact(): Promise<boolean> {
534
+ for (const { dir, state } of await this.treeRecords()) {
535
+ if (!state.completion) continue;
536
+ if (
537
+ state.completion.artifacts.some((p) => {
538
+ const safe = safeArtifactPath(this.workingDir, p);
539
+ return !safe || !existsSync(join(this.workingDir, safe));
540
+ })
541
+ )
542
+ return false;
543
+ if (
544
+ state.completion.stateFiles.some((p) => {
545
+ const safe = safeArtifactPath(dir, p);
546
+ return !safe || !existsSync(join(dir, safe));
547
+ })
548
+ )
549
+ return false;
550
+ try {
551
+ JSON.parse(await readFile(join(dir, 'return.json'), 'utf8'));
552
+ } catch {
553
+ return false;
554
+ }
555
+ }
556
+ return true;
569
557
  }
570
558
 
559
+ // ---- 补充要求 ----
560
+
571
561
  /**
572
- * 扫描 checkpoints/ 目录,返回按 steps 数组顺序排列的已完成 step id 列表。
573
- * 只返回在 steps[] 中存在的 id(防止 YAML 改动后 orphaned checkpoint 干扰恢复)。
562
+ * 追加一条用户补充要求。去重:已有条目的前缀重复不再追加。
563
+ *
564
+ * 持久化的理由:补充要求要与原始目标**同等贯穿始终**,跨进程 resume 时不能丢。
574
565
  */
575
- async scanCheckpoints(steps: StepDef[]): Promise<string[]> {
576
- const cpDir = join(this.dir, CHECKPOINTS_DIR);
577
- const validIds = new Set(steps.map((s) => s.id));
578
- try {
579
- const files = await readdir(cpDir);
580
- const ids: string[] = [];
581
- for (const f of files) {
582
- if (!f.endsWith('.json')) continue;
583
- const stepId = f.replace(/\.json$/, '');
584
- if (validIds.has(stepId)) ids.push(stepId);
585
- }
586
- // 按 steps 数组中的原始顺序排列(非文件系统顺序)
587
- return ids.sort((a, b) => {
588
- const ai = steps.findIndex((s) => s.id === a);
589
- const bi = steps.findIndex((s) => s.id === b);
590
- return ai - bi;
591
- });
592
- } catch {
593
- return [];
594
- }
566
+ async appendSupplement(text: string): Promise<void> {
567
+ const t = text.trim();
568
+ if (!t) return;
569
+ const list = await this.readSupplements();
570
+ if (list.some((s) => s.startsWith(t.slice(0, 240)))) return;
571
+ list.push(t);
572
+ await mkdir(this.dir, { recursive: true });
573
+ await writeFile(
574
+ this.supplementsPath,
575
+ JSON.stringify(list, null, 2),
576
+ 'utf8',
577
+ );
595
578
  }
596
579
 
597
- /** 加载最近一次成功的 vars 快照 */
598
- async loadLastGoodVars(): Promise<Record<string, unknown> | null> {
580
+ async readSupplements(): Promise<string[]> {
599
581
  try {
600
- const raw = await readFile(join(this.dir, 'vars.json'), 'utf-8');
601
- return JSON.parse(raw);
582
+ const parsed = JSON.parse(await readFile(this.supplementsPath, 'utf8'));
583
+ return Array.isArray(parsed) ? parsed.map((s) => String(s)) : [];
602
584
  } catch {
603
- return null;
585
+ return [];
604
586
  }
605
587
  }
606
588
 
607
- /** 加载所有已完成步骤的 capture 变量(用于 resume 时回放到 VarStack) */
608
- async loadAllCaptures(stepIds: string[]): Promise<Record<string, Record<string, unknown>>> {
609
- const result: Record<string, Record<string, unknown>> = {};
610
- const cpDir = join(this.dir, CHECKPOINTS_DIR);
611
- for (const sid of stepIds) {
589
+ // ---- 人类可读进度 ----
590
+
591
+ /**
592
+ * 追加一行进度。
593
+ *
594
+ * 存在理由:TUI 的 scrollback 会被刷掉,而排障常常发生在「跑完之后」。
595
+ * 落盘的 progress.md 是唯一还能回看的现场(provider 的真实报错也写在这里)。
596
+ */
597
+ async appendProgress(line: string): Promise<void> {
598
+ this.progressWrites = this.progressWrites.then(async () => {
612
599
  try {
613
- const raw = await readFile(join(cpDir, `${sid}.json`), 'utf-8');
614
- const cp: CheckpointData = JSON.parse(raw);
615
- if (cp.captures && Object.keys(cp.captures).length > 0) {
616
- result[sid] = cp.captures;
617
- }
600
+ await mkdir(this.dir, { recursive: true });
601
+ await appendFile(this.progressPath, `${line}\n`, 'utf8');
618
602
  } catch {
619
- // 单个 checkpoint 读失败不阻断整体
603
+ // 进度日志写失败不该影响任务本身
620
604
  }
621
- }
622
- return result;
623
- }
624
-
625
- // ---- 私有方法:声明式产物生命周期 ----
626
-
627
- /** 删除工作流【声明】的产物文件(相对 cwd),用于 new-task 清理旧任务残留(如旧 PLAN.md)。
628
- * 仅删工作流自己声明的产物、仅 force-rm 文件,scoped 安全。返回清理过的路径列表。 */
629
- private async cleanupDeclaredArtifacts(paths: string[]): Promise<string[]> {
630
- for (const p of paths) {
631
- await rm(join(this.workingDir, p), { force: true }).catch(() => {});
632
- }
633
- return paths;
605
+ });
606
+ await this.progressWrites;
634
607
  }
635
608
 
636
- /** 从声明产物路径里过滤出 cwd 下【实际存在】的文件(completed 报真实产物用)。 */
637
- private existingArtifacts(paths: string[]): string[] {
638
- return paths.filter((p) => existsSync(join(this.workingDir, p)));
609
+ async appendEvent(event: unknown): Promise<void> {
610
+ this.eventWrites = this.eventWrites.then(async () => {
611
+ try {
612
+ await mkdir(this.dir, { recursive: true });
613
+ await appendFile(this.eventsPath, `${JSON.stringify(event)}\n`, 'utf8');
614
+ } catch {
615
+ // Structured diagnostics must never fail the workflow itself.
616
+ }
617
+ });
618
+ await this.eventWrites;
639
619
  }
640
620
 
641
- /** 清理【实际存在】的本次声明产物(fresh/new-task 启动时清残留孤儿),返回真正删掉的路径。 */
642
- private async cleanupStaleDeclared(declaredArtifacts: string[]): Promise<string[]> {
643
- return this.cleanupDeclaredArtifacts(this.existingArtifacts(declaredArtifacts));
621
+ /**
622
+ * 等待所有已排队的 progress/event 写入落盘(不做归档,无其它副作用)。
623
+ *
624
+ * `journaled()`(`runtime/context.ts`)里的 appendProgress 调用是 fire-and-forget
625
+ * ——生产路径的落盘保证来自 `archive()` 在 `runner.ts` 终止路径上对同一条
626
+ * `progressWrites`/`eventWrites` 链的 await。**绕开 `runner.ts` 直接调用能力对象
627
+ * 的测试**(例如只起 `createWorkflowContext()` 就调 `wf.model.generate()`)没有
628
+ * 这条终态保证,读 `progress.md`/`events.jsonl` 前必须显式调用这个方法。
629
+ */
630
+ async flush(): Promise<void> {
631
+ await Promise.all([this.progressWrites, this.eventWrites]);
644
632
  }
645
633
 
646
- // ---- 公共方法:归档与清理(保持原有签名兼容) ----
634
+ // ---- 归档与清理 ----
647
635
 
648
- /**
649
- * 归档(成功完成时调用):重命名状态目录为 .done 后缀,保留所有中间产物。
650
- * vars.json / progress.md / inputs.json / checkpoints/ 全部保留——供后续对话 Read 回溯。
651
- */
636
+ /** 成功收尾:`<taskId>/` → `<taskId>.done/`。 */
652
637
  async archive(): Promise<void> {
653
- const doneDir = this.dir + DONE_SUFFIX;
654
- try {
655
- await rm(doneDir, { recursive: true, force: true });
656
- } catch {
657
- // ignore
658
- }
638
+ await this.flush();
639
+ if (!this.dir || !existsSync(this.dir)) return;
640
+ const dest = this.dir.endsWith(DONE_SUFFIX)
641
+ ? this.dir
642
+ : this.dir + DONE_SUFFIX;
643
+ if (dest === this.dir) return;
659
644
  try {
660
- await rename(this.dir, doneDir);
645
+ await rm(dest, { recursive: true, force: true });
646
+ await rename(this.dir, dest);
647
+ this.dir = dest;
661
648
  } catch {
662
- // rename 失败时静默(某些文件系统/权限问题),保留原目录
649
+ // 归档失败不影响任务结论,下次 init 会按未完成处理
663
650
  }
664
651
  }
665
652
 
666
- /** 强制清理(/new 或新任务 init 时调用):删除状态目录(含 .done 归档) */
653
+ /** 删掉本实例的状态目录(不碰 `.trash/`,那是用户的找回通道)。 */
667
654
  async cleanup(): Promise<void> {
668
- await rm(this.dir, { recursive: true, force: true }).catch(() => {});
669
- await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => {});
655
+ if (!this.dir) return;
656
+ try {
657
+ await rm(this.dir, { recursive: true, force: true });
658
+ } catch {
659
+ /* 忽略 */
660
+ }
670
661
  }
671
662
 
672
- // ---- 私有方法 ----
673
-
674
- /** 创建全新的空白状态目录(含 checkpoints/ 子目录),并记录声明产物清单。 */
675
- private async cleanSlate(
676
- name: string,
677
- inputs: Record<string, unknown>,
678
- taskId: string,
679
- declaredArtifacts: string[] = [],
680
- defFingerprint?: string,
681
- ): Promise<void> {
682
- await this.cleanup();
683
- await mkdir(this.dir, { recursive: true });
684
- await mkdir(join(this.dir, CHECKPOINTS_DIR), { recursive: true });
663
+ // ---- 声明产物的生命周期 ----
685
664
 
686
- await this.writeCurrent({
687
- name,
688
- taskId,
689
- status: 'running',
690
- startedAt: new Date().toISOString(),
691
- artifacts: declaredArtifacts,
692
- defFingerprint,
693
- });
694
-
695
- await writeFile(join(this.dir, 'inputs.json'), JSON.stringify(inputs, null, 2), 'utf-8');
696
- await writeFile(join(this.dir, 'progress.md'), `# ${name}\n\n`, 'utf-8');
665
+ /**
666
+ * 把声明里的 glob(`research/04-*.md`)展开成 cwd 下实际匹配的相对路径;
667
+ * 非 glob 原样保留。越界路径被过滤(fail-closed)。
668
+ * 声明里写了 glob 却一个都不匹配 → 该声明被丢弃(不存在的产物不参与清理/报告)。
669
+ */
670
+ private async expandDeclaredArtifacts(paths: string[]): Promise<string[]> {
671
+ const out: string[] = [];
672
+ for (const p of paths) {
673
+ const safe = safeArtifactPath(this.workingDir, p);
674
+ if (safe === null) continue;
675
+ if (!/[*?]/.test(safe)) {
676
+ // ⚠️ 必须归一成 `/`:`safeArtifactPath` 返回的是**平台分隔符**,而下面
677
+ // glob 分支里 walkGlob 拼出来的一律是 `/`。两个分支不统一的话,同一份
678
+ // 声明列表在 Windows 上会同时产出 `out\a.md` 与 `out/b.md` —— 去重
679
+ // (new Set)失效、与 current.json 里存的旧值比不上、报给用户的路径
680
+ // 时而斜杠时而反斜杠。`join()` 吃 `/` 没问题,所以统一到 `/` 是安全的。
681
+ out.push(safe.replace(/\\/g, '/'));
682
+ continue;
683
+ }
684
+ const segs = safe.replace(/\\/g, '/').split('/');
685
+ const fullRe = new RegExp(
686
+ `^${segs.map(artifactSegToPattern).join('/')}$`,
687
+ );
688
+ out.push(...(await this.walkGlob(this.workingDir, segs, fullRe, '')));
689
+ }
690
+ return out;
697
691
  }
698
692
 
699
- /** 读取旧任务的 inputs.json(仲裁时取旧 goal 用),失败返回 null */
700
- private async readInputs(): Promise<Record<string, unknown> | null> {
693
+ private async walkGlob(
694
+ absDir: string,
695
+ segs: string[],
696
+ fullRe: RegExp,
697
+ relPrefix: string,
698
+ ): Promise<string[]> {
699
+ const [head, ...rest] = segs;
700
+ if (!head) return [];
701
+ let entries: Dirent[];
701
702
  try {
702
- const raw = await readFile(join(this.dir, 'inputs.json'), 'utf-8');
703
- return JSON.parse(raw);
703
+ entries = await readdir(absDir, { withFileTypes: true });
704
704
  } catch {
705
- return null;
705
+ return [];
706
706
  }
707
+ const headRe = new RegExp(`^${artifactSegToPattern(head)}$`);
708
+ const results: string[] = [];
709
+ for (const e of entries) {
710
+ if (!headRe.test(e.name)) continue;
711
+ const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
712
+ if (rest.length === 0) {
713
+ if (e.isFile() && fullRe.test(rel)) results.push(rel);
714
+ } else if (e.isDirectory()) {
715
+ results.push(
716
+ ...(await this.walkGlob(join(absDir, e.name), rest, fullRe, rel)),
717
+ );
718
+ }
719
+ }
720
+ return results;
707
721
  }
708
722
 
709
- /** 读取 current.json,解析失败或文件不存在返回 null */
710
- private async readCurrentState(): Promise<CurrentState | null> {
711
- try {
712
- const raw = await readFile(join(this.dir, 'current.json'), 'utf-8');
713
- return JSON.parse(raw);
714
- } catch {
715
- return null;
723
+ /** 声明产物里**实际存在**的那些(completed 报告用;越界的视为不存在)。 */
724
+ private async existingArtifacts(paths: string[]): Promise<string[]> {
725
+ const expanded = await this.expandDeclaredArtifacts(paths);
726
+ return [...new Set(expanded)].filter((rel) =>
727
+ existsSync(join(this.workingDir, rel)),
728
+ );
729
+ }
730
+
731
+ /**
732
+ * 把实际存在的声明产物移入 `<实例目录>.trash/<stamp>/`,保留相对路径结构。
733
+ * 一个都没成功移走时返回 undefined —— **绝不谎报归档位置**(见文件头注)。
734
+ */
735
+ private async archiveDeclaredArtifacts(
736
+ existingRels: string[],
737
+ stamp: string,
738
+ ): Promise<string | undefined> {
739
+ if (existingRels.length === 0) return undefined;
740
+ const trashRoot = join(this.dir + TRASH_SUFFIX, stamp);
741
+ let archived = 0;
742
+ for (const rel of existingRels) {
743
+ const dest = join(trashRoot, rel);
744
+ try {
745
+ await mkdir(dirname(dest), { recursive: true });
746
+ await rename(join(this.workingDir, rel), dest);
747
+ archived++;
748
+ } catch {
749
+ // 单个失败不阻断,残留交给后续 rm 兜底
750
+ }
716
751
  }
752
+ if (archived === 0) return undefined;
753
+ const display = relative(this.workingDir, trashRoot);
754
+ return display.startsWith('..') ? trashRoot : display;
717
755
  }
718
756
 
719
- /** 读取归档目录(.done)的 current.json,用于 completed 检测(成功后 dir 被 archive 改名)。 */
720
- private async readArchivedState(): Promise<CurrentState | null> {
721
- try {
722
- const raw = await readFile(join(this.dir + DONE_SUFFIX, 'current.json'), 'utf-8');
723
- return JSON.parse(raw);
724
- } catch {
725
- return null;
757
+ /** rm 兜底:只删工作流自己声明的产物,且路径强制在 cwd 内。 */
758
+ private async cleanupDeclaredArtifacts(paths: string[]): Promise<string[]> {
759
+ const done: string[] = [];
760
+ for (const rel of await this.expandDeclaredArtifacts(paths)) {
761
+ const abs = join(this.workingDir, rel);
762
+ if (!existsSync(abs)) continue;
763
+ try {
764
+ await rm(abs, { force: true });
765
+ done.push(rel);
766
+ } catch {
767
+ /* 单个失败不影响整体 */
768
+ }
726
769
  }
770
+ return done;
727
771
  }
728
772
 
729
- /** 写入 current.json */
730
- private async writeCurrent(data: CurrentState): Promise<void> {
731
- await writeFile(join(this.dir, 'current.json'), JSON.stringify(data, null, 2), 'utf-8');
773
+ /** 「先归档再清理」组合。fresh/new-task 的启动清理统一走这里。 */
774
+ private async archiveThenClean(
775
+ paths: string[],
776
+ stamp: string,
777
+ ): Promise<{ cleaned: string[]; archivedTo?: string }> {
778
+ const existing = await this.existingArtifacts(paths);
779
+ const archivedTo = await this.archiveDeclaredArtifacts(existing, stamp);
780
+ const cleaned = await this.cleanupDeclaredArtifacts(paths);
781
+ return { cleaned, archivedTo };
732
782
  }
733
783
  }