@pixelbyte-software/pixcode 1.51.2 → 1.51.3

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 (320) hide show
  1. package/CODE_OF_CONDUCT.md +41 -41
  2. package/CONTRIBUTING.md +155 -155
  3. package/LICENSE +718 -718
  4. package/README.de.md +169 -169
  5. package/README.ja.md +167 -167
  6. package/README.ko.md +167 -167
  7. package/README.md +419 -419
  8. package/README.ru.md +169 -169
  9. package/README.tr.md +298 -298
  10. package/README.zh-CN.md +167 -167
  11. package/SECURITY.md +46 -46
  12. package/dist/api-automation.html +110 -110
  13. package/dist/api-docs.html +548 -548
  14. package/dist/assets/{index-EN9ngyxf.js → index-17CwxHSZ.js} +185 -185
  15. package/dist/assets/index-B9N-gfOQ.css +32 -0
  16. package/dist/clear-cache.html +85 -85
  17. package/dist/convert-icons.md +52 -52
  18. package/dist/docs.html +308 -308
  19. package/dist/favicon.svg +8 -8
  20. package/dist/features.html +133 -133
  21. package/dist/generate-icons.js +48 -48
  22. package/dist/humans.txt +15 -15
  23. package/dist/icons/codex-white.svg +3 -3
  24. package/dist/icons/codex.svg +3 -3
  25. package/dist/icons/cursor-white.svg +11 -11
  26. package/dist/icons/icon-128x128.svg +9 -9
  27. package/dist/icons/icon-144x144.svg +9 -9
  28. package/dist/icons/icon-152x152.svg +9 -9
  29. package/dist/icons/icon-192x192.svg +9 -9
  30. package/dist/icons/icon-384x384.svg +9 -9
  31. package/dist/icons/icon-512x512.svg +9 -9
  32. package/dist/icons/icon-72x72.svg +9 -9
  33. package/dist/icons/icon-96x96.svg +9 -9
  34. package/dist/icons/icon-template.svg +9 -9
  35. package/dist/icons/qwen-logo.svg +14 -14
  36. package/dist/index.html +59 -59
  37. package/dist/landing.html +268 -268
  38. package/dist/llms-full.txt +119 -119
  39. package/dist/llms.txt +53 -53
  40. package/dist/logo.svg +12 -12
  41. package/dist/manifest.json +60 -60
  42. package/dist/openapi.yaml +1696 -1696
  43. package/dist/orchestration.html +125 -125
  44. package/dist/robots.txt +4 -4
  45. package/dist/site.css +692 -692
  46. package/dist/sitemap.xml +51 -51
  47. package/dist/sw.js +132 -132
  48. package/dist-server/server/cli.js +96 -96
  49. package/dist-server/server/daemon/manager.js +33 -33
  50. package/dist-server/server/daemon-manager.js +64 -64
  51. package/dist-server/server/index.js +122 -3
  52. package/dist-server/server/index.js.map +1 -1
  53. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js +84 -0
  54. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js.map +1 -0
  55. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js +43 -0
  56. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js.map +1 -0
  57. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +55 -1
  58. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
  59. package/dist-server/server/modules/orchestration/index.js +1 -0
  60. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  61. package/dist-server/server/routes/commands.js +25 -25
  62. package/dist-server/server/routes/git.js +17 -17
  63. package/dist-server/server/routes/live-view.js +46 -46
  64. package/dist-server/server/services/hermes-gateway.js +310 -0
  65. package/dist-server/server/services/hermes-gateway.js.map +1 -1
  66. package/dist-server/server/services/public-api-manifest.js +59 -51
  67. package/dist-server/server/services/public-api-manifest.js.map +1 -1
  68. package/package.json +222 -222
  69. package/scripts/fix-node-pty.js +67 -67
  70. package/scripts/github/create-v1.38-issues.mjs +351 -351
  71. package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
  72. package/scripts/hermes/configure-pixcode-mcp.mjs +165 -163
  73. package/scripts/hermes/pixcode-mcp-server.mjs +1009 -958
  74. package/scripts/smoke/changes-panel-layout.mjs +48 -48
  75. package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
  76. package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
  77. package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
  78. package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
  79. package/scripts/smoke/chat-session-state.mjs +19 -19
  80. package/scripts/smoke/code-editor-theme.mjs +55 -55
  81. package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
  82. package/scripts/smoke/command-center-agent-writes.mjs +79 -79
  83. package/scripts/smoke/command-center-non-git.mjs +46 -46
  84. package/scripts/smoke/context-packet.mjs +43 -43
  85. package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
  86. package/scripts/smoke/daemon-entrypoint.mjs +20 -20
  87. package/scripts/smoke/default-landing-routing.mjs +33 -33
  88. package/scripts/smoke/desktop-native-notifications.mjs +30 -30
  89. package/scripts/smoke/desktop-tray-icon.mjs +33 -33
  90. package/scripts/smoke/discord-release-workflow.mjs +24 -24
  91. package/scripts/smoke/git-install-update.mjs +255 -255
  92. package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
  93. package/scripts/smoke/hermes-api-install.mjs +56 -56
  94. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
  95. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -367
  96. package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
  97. package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
  98. package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
  99. package/scripts/smoke/hermes-rest-gateway.mjs +79 -70
  100. package/scripts/smoke/hermes-rest-live.mjs +42 -42
  101. package/scripts/smoke/hermes-roundtrip.mjs +167 -167
  102. package/scripts/smoke/hermes-settings-commands.mjs +349 -346
  103. package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
  104. package/scripts/smoke/live-view-diagnostics.mjs +53 -53
  105. package/scripts/smoke/live-view-environment.mjs +92 -92
  106. package/scripts/smoke/live-view-integration.mjs +450 -450
  107. package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
  108. package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
  109. package/scripts/smoke/model-registry.mjs +36 -36
  110. package/scripts/smoke/multi-project-ui.mjs +45 -45
  111. package/scripts/smoke/multi-worker-slots.mjs +42 -42
  112. package/scripts/smoke/notification-center.mjs +87 -87
  113. package/scripts/smoke/notification-inapp-preference.mjs +23 -23
  114. package/scripts/smoke/notification-taxonomy.mjs +58 -58
  115. package/scripts/smoke/orchestration-api.mjs +172 -172
  116. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
  117. package/scripts/smoke/orchestration-live-run.mjs +176 -176
  118. package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
  119. package/scripts/smoke/orchestration-model-sync.mjs +30 -30
  120. package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
  121. package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
  122. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
  123. package/scripts/smoke/permission-policy.mjs +50 -50
  124. package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
  125. package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
  126. package/scripts/smoke/provider-rest-api.mjs +124 -124
  127. package/scripts/smoke/provider-selection-status.mjs +52 -52
  128. package/scripts/smoke/run-state-refresh.mjs +52 -52
  129. package/scripts/smoke/runtime-manager.mjs +99 -99
  130. package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
  131. package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
  132. package/scripts/smoke/static-root-routing.mjs +21 -21
  133. package/scripts/smoke/strict-handoff-compact.mjs +60 -60
  134. package/scripts/smoke/taskmaster-config.mjs +24 -24
  135. package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
  136. package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
  137. package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
  138. package/scripts/smoke/telegram-control.mjs +242 -242
  139. package/scripts/smoke/tunnel-persistence.mjs +56 -56
  140. package/scripts/smoke/update-issue-progress.mjs +69 -69
  141. package/scripts/smoke/update-ux.mjs +55 -55
  142. package/scripts/smoke/v138-completion.mjs +132 -132
  143. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
  144. package/scripts/smoke/v138-diagnostics.mjs +63 -63
  145. package/scripts/smoke/v138-issue-planner.mjs +33 -33
  146. package/scripts/smoke/v143-remote-control.mjs +76 -76
  147. package/scripts/smoke/v144-production-loop.mjs +47 -47
  148. package/scripts/smoke/v145-platformization.mjs +46 -46
  149. package/scripts/smoke/v146-control-room-ui.mjs +150 -150
  150. package/scripts/smoke/version-modal-autoshow.mjs +29 -29
  151. package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
  152. package/scripts/smoke/vscode-workbench-polish.mjs +461 -436
  153. package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
  154. package/scripts/smoke/workflow-templates.mjs +43 -43
  155. package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
  156. package/scripts/update-git-install.mjs +293 -293
  157. package/server/claude-sdk.js +920 -920
  158. package/server/cli.js +1039 -1039
  159. package/server/constants/config.js +4 -4
  160. package/server/cursor-cli.js +344 -344
  161. package/server/daemon/manager.js +563 -563
  162. package/server/daemon-manager.js +964 -964
  163. package/server/database/db.js +921 -921
  164. package/server/database/json-store.js +197 -197
  165. package/server/gemini-cli.js +550 -550
  166. package/server/gemini-response-handler.js +79 -79
  167. package/server/index.js +128 -2
  168. package/server/load-env.js +35 -35
  169. package/server/middleware/auth.js +175 -175
  170. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
  171. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
  172. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
  173. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
  174. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
  175. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
  176. package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -0
  177. package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -0
  178. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
  179. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
  180. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  181. package/server/modules/orchestration/a2a/routes.ts +590 -590
  182. package/server/modules/orchestration/a2a/task-store.ts +178 -178
  183. package/server/modules/orchestration/a2a/types.ts +126 -126
  184. package/server/modules/orchestration/a2a/validator.ts +113 -113
  185. package/server/modules/orchestration/hermes/hermes.routes.ts +642 -583
  186. package/server/modules/orchestration/index.ts +101 -100
  187. package/server/modules/orchestration/preview/port-watcher.ts +112 -112
  188. package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
  189. package/server/modules/orchestration/preview/types.ts +19 -19
  190. package/server/modules/orchestration/security/permission-policy.ts +401 -401
  191. package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
  192. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
  193. package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
  194. package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
  195. package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
  196. package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
  197. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
  198. package/server/modules/orchestration/workflows/context-packet.ts +186 -186
  199. package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
  200. package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
  201. package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
  202. package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
  203. package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
  204. package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
  205. package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
  206. package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
  207. package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
  208. package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
  209. package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
  210. package/server/modules/orchestration/workspace/path-safety.ts +55 -55
  211. package/server/modules/orchestration/workspace/types.ts +52 -52
  212. package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
  213. package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
  214. package/server/modules/providers/index.ts +2 -2
  215. package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
  216. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  217. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  218. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  219. package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
  220. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  221. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  222. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  223. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
  224. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  225. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  226. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  227. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
  228. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  229. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  230. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  231. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
  232. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  233. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
  234. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  235. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
  236. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  237. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  238. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  239. package/server/modules/providers/provider.registry.ts +40 -40
  240. package/server/modules/providers/provider.routes.ts +944 -944
  241. package/server/modules/providers/services/mcp.service.ts +86 -86
  242. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  243. package/server/modules/providers/services/sessions.service.ts +45 -45
  244. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  245. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  246. package/server/modules/providers/shared/provider-configs.ts +142 -142
  247. package/server/modules/providers/tests/mcp.test.ts +293 -293
  248. package/server/openai-codex.js +462 -462
  249. package/server/opencode-cli.js +491 -491
  250. package/server/opencode-response-handler.js +111 -111
  251. package/server/projects.js +3008 -3008
  252. package/server/qwen-code-cli.js +410 -410
  253. package/server/qwen-response-handler.js +73 -73
  254. package/server/routes/agent.js +1435 -1435
  255. package/server/routes/auth.js +159 -159
  256. package/server/routes/codex.js +20 -20
  257. package/server/routes/commands.js +570 -570
  258. package/server/routes/cursor.js +61 -61
  259. package/server/routes/diagnostics.js +41 -41
  260. package/server/routes/gemini.js +25 -25
  261. package/server/routes/git.js +1650 -1650
  262. package/server/routes/live-view.js +411 -411
  263. package/server/routes/mcp-utils.js +13 -13
  264. package/server/routes/messages.js +62 -62
  265. package/server/routes/network.js +125 -125
  266. package/server/routes/platformization.js +212 -212
  267. package/server/routes/plugins.js +320 -320
  268. package/server/routes/production-agent-loop.js +90 -90
  269. package/server/routes/projects.js +917 -917
  270. package/server/routes/public-api.js +34 -34
  271. package/server/routes/qwen.js +27 -27
  272. package/server/routes/remote.js +55 -55
  273. package/server/routes/settings.js +321 -321
  274. package/server/routes/telegram.js +140 -140
  275. package/server/routes/user.js +125 -125
  276. package/server/routes/webhooks.js +63 -63
  277. package/server/services/control-room.js +102 -102
  278. package/server/services/diagnostics.js +165 -165
  279. package/server/services/external-access.js +375 -375
  280. package/server/services/hermes-gateway.js +1562 -1247
  281. package/server/services/hermes-install-jobs.js +729 -729
  282. package/server/services/install-jobs.js +715 -715
  283. package/server/services/live-view.js +956 -956
  284. package/server/services/managed-runtimes.js +493 -493
  285. package/server/services/model-registry.js +144 -144
  286. package/server/services/notification-orchestrator.js +365 -365
  287. package/server/services/notification-taxonomy.js +204 -204
  288. package/server/services/platformization.js +815 -815
  289. package/server/services/production-agent-loop.js +248 -248
  290. package/server/services/provider-cli-versions.js +149 -149
  291. package/server/services/provider-credentials.js +189 -189
  292. package/server/services/provider-models.js +396 -396
  293. package/server/services/public-api-manifest.js +190 -182
  294. package/server/services/remote-connection.js +127 -127
  295. package/server/services/runtime-manager.js +323 -323
  296. package/server/services/startup-update.js +234 -234
  297. package/server/services/telegram/bot.js +331 -331
  298. package/server/services/telegram/control-center.js +979 -979
  299. package/server/services/telegram/telegram-http-client.js +151 -151
  300. package/server/services/telegram/translations.js +340 -340
  301. package/server/services/vapid-keys.js +36 -36
  302. package/server/services/webhooks.js +216 -216
  303. package/server/sessionManager.js +225 -225
  304. package/server/shared/interfaces.ts +54 -54
  305. package/server/shared/types.ts +172 -172
  306. package/server/shared/utils.ts +193 -193
  307. package/server/tsconfig.json +36 -36
  308. package/server/utils/colors.js +21 -21
  309. package/server/utils/commandParser.js +305 -305
  310. package/server/utils/frontmatter.js +18 -18
  311. package/server/utils/gitConfig.js +34 -34
  312. package/server/utils/plugin-loader.js +457 -457
  313. package/server/utils/plugin-process-manager.js +185 -185
  314. package/server/utils/port-access.js +209 -209
  315. package/server/utils/runtime-paths.js +37 -37
  316. package/server/utils/url-detection.js +71 -71
  317. package/server/vite-daemon.js +79 -79
  318. package/shared/modelConstants.js +161 -161
  319. package/shared/networkHosts.js +22 -22
  320. package/dist/assets/index-DMz0zv6T.css +0 -32
@@ -1,2070 +1,2070 @@
1
- import crypto from 'node:crypto';
2
-
3
- import type {
4
- Workflow,
5
- WorkflowNode,
6
- WorkflowNodeRun,
7
- WorkflowRun,
8
- } from '@/modules/orchestration/workflows/workflow.types.js';
9
- import {
10
- PIXCODE_HANDOFF_PROTOCOL,
11
- formatHandoffArtifactForContext,
12
- handoffArtifactToWorkflowArtifact,
13
- parseHandoffArtifact,
14
- } from '@/modules/orchestration/workflows/handoff-artifact.js';
15
- import {
16
- buildWorkflowContextPacket,
17
- formatContextPacketForPrompt,
18
- } from '@/modules/orchestration/workflows/context-packet.js';
19
- import {
20
- type WorkflowFallbackTrigger,
21
- classifyWorkflowFailure,
22
- resolveWorkflowFallbackDecision,
23
- } from '@/modules/orchestration/workflows/workflow-fallback-policy.js';
24
- import {
25
- evaluatePermissionRequest,
26
- resolvePermissionPolicyFromMetadata,
27
- type PermissionDecision,
28
- type PermissionPolicy,
29
- type PermissionPolicyEvent,
30
- } from '@/modules/orchestration/security/permission-policy.js';
31
- import {
32
- type ResolvedWorkspaceTarget,
33
- resolveWorkflowWorkspace,
34
- workspaceContextPrompt,
35
- workspaceTargetMetadata,
36
- } from '@/modules/orchestration/workflows/workspace-target.js';
37
- import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
38
- import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
39
- // @ts-ignore — plain-JS service
40
- import {
41
- getDefaultProviderModel,
42
- getProviderModelRegistryEntry,
43
- getStaticProviderModels,
44
- } from '@/services/model-registry.js';
45
- // @ts-ignore — plain-JS service
46
- import {
47
- createNotificationEvent,
48
- notifyRunFailed,
49
- notifyRunStopped,
50
- notifyUserIfEnabled,
51
- } from '@/services/notification-orchestrator.js';
52
- // @ts-ignore — plain-JS service
53
- import { dispatchWebhookEvent } from '@/services/webhooks.js';
54
-
55
- const TERMINAL = new Set(['completed', 'failed', 'canceled']);
56
- const SKIPPED = 'skipped';
57
- const BACKEND_HANDOFF_TIMEOUT_MS = 120_000;
58
- const MAX_OUTPUT_CONTEXT_CHARS = 12_000;
59
- const DEFAULT_MAX_REPAIR_CYCLES = 1;
60
- const MAX_REPAIR_CYCLES = 5;
61
- const HANDOFF_ARTIFACT_EXAMPLE = [
62
- '{',
63
- ' "protocol": "pixcode.handoff.v1",',
64
- ' "taskStatus": "ready | completed | blocked | failed | needs-review",',
65
- ' "contextSummary": "Compacted context the next agent needs.",',
66
- ' "taskResult": "What was decided or completed in this step.",',
67
- ' "changedFiles": [],',
68
- ' "blockers": [],',
69
- ' "risks": [],',
70
- ' "nextAction": "The requested next action.",',
71
- ' "nextInstructions": "Specific instructions for the next agent."',
72
- '}',
73
- ].join('\n');
74
- const KNOWN_AGENT_ROLES = [
75
- 'backend',
76
- 'frontend',
77
- 'review',
78
- 'implementation',
79
- 'proposal',
80
- 'critique',
81
- 'response',
82
- 'decision',
83
- 'report',
84
- ] as const;
85
-
86
- class WorkflowCanceledError extends Error {
87
- constructor() {
88
- super('Workflow canceled.');
89
- this.name = 'WorkflowCanceledError';
90
- }
91
- }
92
-
93
- class WorkflowNodeTimeoutError extends Error {
94
- constructor(readonly timeoutMs: number) {
95
- super(`Workflow node timed out after ${Math.round(timeoutMs / 1000)}s.`);
96
- this.name = 'WorkflowNodeTimeoutError';
97
- }
98
- }
99
-
100
- function newId(prefix: string): string {
101
- return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
102
- }
103
-
104
- function localHermesBaseUrl(): string {
105
- return `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}/hermes`;
106
- }
107
-
108
- function validateWorkflow(workflow: Workflow): void {
109
- if (workflow.nodes.length > 64) {
110
- throw new Error('Workflow node limit exceeded.');
111
- }
112
- const ids = new Set(workflow.nodes.map((node) => node.id));
113
- for (const node of workflow.nodes) {
114
- for (const input of node.inputs) {
115
- if (!ids.has(input)) {
116
- throw new Error(`Workflow node ${node.id} references missing input ${input}.`);
117
- }
118
- }
119
- }
120
- }
121
-
122
- type TaskResult = {
123
- state: string;
124
- text: string;
125
- error?: string;
126
- messages: Array<{ role: string; text: string }>;
127
- artifacts: Array<{
128
- type: string;
129
- text?: string;
130
- data?: Record<string, unknown>;
131
- metadata?: Record<string, unknown>;
132
- }>;
133
- };
134
-
135
- type RawTask = {
136
- state?: string;
137
- error?: { code?: string; message?: string };
138
- history?: Array<{ role?: string; parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }> }>;
139
- artifacts?: Array<{
140
- type?: string;
141
- parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }>;
142
- metadata?: Record<string, unknown>;
143
- }>;
144
- };
145
-
146
- type AgentAssignment = {
147
- instanceId: string;
148
- adapterId: string;
149
- label: string;
150
- role?: AgentRole;
151
- instruction?: string;
152
- model?: string;
153
- permissionMode?: string;
154
- toolsSettings?: Record<string, unknown>;
155
- order: number;
156
- };
157
-
158
- type KnownAgentRole = typeof KNOWN_AGENT_ROLES[number];
159
- type AgentRole = string;
160
- type ProviderId = 'claude' | 'cursor' | 'codex' | 'gemini' | 'qwen' | 'opencode';
161
- type ProviderModel = {
162
- value: string;
163
- label?: string;
164
- source?: 'static' | 'api';
165
- free?: boolean;
166
- };
167
- type RunStoppedNotifier = (payload: {
168
- userId: string | number;
169
- provider: string;
170
- sessionId?: string | null;
171
- stopReason?: string;
172
- sessionName?: string | null;
173
- }) => void;
174
- type RunFailedNotifier = (payload: {
175
- userId: string | number;
176
- provider: string;
177
- sessionId?: string | null;
178
- error: unknown;
179
- sessionName?: string | null;
180
- }) => void;
181
-
182
- const sendRunStoppedNotification = notifyRunStopped as RunStoppedNotifier;
183
- const sendRunFailedNotification = notifyRunFailed as RunFailedNotifier;
184
-
185
- const adapterProviderMap: Record<string, ProviderId | undefined> = {
186
- 'claude-code': 'claude',
187
- cursor: 'cursor',
188
- codex: 'codex',
189
- gemini: 'gemini',
190
- qwen: 'qwen',
191
- opencode: 'opencode',
192
- };
193
-
194
- function readAgentRole(value: unknown): AgentRole | undefined {
195
- return typeof value === 'string' && value.trim() && value.trim() !== 'auto'
196
- ? value.trim()
197
- : undefined;
198
- }
199
-
200
- function isKnownAgentRole(value: string | undefined): value is KnownAgentRole {
201
- return Boolean(value && (KNOWN_AGENT_ROLES as readonly string[]).includes(value));
202
- }
203
-
204
- function getMetadataRecord(metadata: Record<string, unknown> | undefined, key: string): Record<string, unknown> {
205
- return readRecord(metadata?.[key]) ?? {};
206
- }
207
-
208
- function readRecord(value: unknown): Record<string, unknown> | undefined {
209
- return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
210
- }
211
-
212
- function readString(value: unknown): string | undefined {
213
- return typeof value === 'string' && value.trim() ? value : undefined;
214
- }
215
-
216
- function readNotificationUserId(metadata?: Record<string, unknown>): string | number | null {
217
- const value = metadata?.userId;
218
- return typeof value === 'string' || typeof value === 'number' ? value : null;
219
- }
220
-
221
- function workflowNotificationTitle(run: WorkflowRun): string {
222
- return readString(run.metadata?.workflowName) ?? run.workflowId;
223
- }
224
-
225
- function notifyWorkflowRunFinished(run: WorkflowRun): void {
226
- const userId = readNotificationUserId(run.metadata);
227
- if (!userId) return;
228
-
229
- if (run.status === 'completed') {
230
- sendRunStoppedNotification({
231
- userId,
232
- provider: 'system',
233
- sessionId: run.id,
234
- sessionName: workflowNotificationTitle(run),
235
- stopReason: 'Orchestration completed',
236
- });
237
- return;
238
- }
239
-
240
- if (run.status === 'canceled') {
241
- sendRunStoppedNotification({
242
- userId,
243
- provider: 'system',
244
- sessionId: run.id,
245
- sessionName: workflowNotificationTitle(run),
246
- stopReason: 'Orchestration canceled',
247
- });
248
- return;
249
- }
250
-
251
- if (run.status === 'failed') {
252
- sendRunFailedNotification({
253
- userId,
254
- provider: 'system',
255
- sessionId: run.id,
256
- sessionName: workflowNotificationTitle(run),
257
- error: readString(run.metadata?.error) ?? 'Orchestration failed',
258
- });
259
- }
260
- }
261
-
262
- function permissionPolicyFromRun(run: WorkflowRun): PermissionPolicy {
263
- return resolvePermissionPolicyFromMetadata(run.metadata);
264
- }
265
-
266
- function permissionPolicyEvents(run: WorkflowRun): PermissionPolicyEvent[] {
267
- return Array.isArray(run.metadata?.permissionPolicyEvents)
268
- ? run.metadata.permissionPolicyEvents.filter((event): event is PermissionPolicyEvent =>
269
- Boolean(event && typeof event === 'object'),
270
- )
271
- : [];
272
- }
273
-
274
- function permissionApprovalRequests(run: WorkflowRun): Array<Record<string, unknown>> {
275
- return Array.isArray(run.metadata?.pendingPermissionApprovals)
276
- ? run.metadata.pendingPermissionApprovals.filter((event): event is Record<string, unknown> =>
277
- Boolean(event && typeof event === 'object'),
278
- )
279
- : [];
280
- }
281
-
282
- function notifyPermissionApprovalRequested(run: WorkflowRun, decision: PermissionDecision): void {
283
- const userId = readNotificationUserId(run.metadata);
284
- if (!userId || !decision.approvalRequest) return;
285
-
286
- const event = (createNotificationEvent as unknown as (payload: Record<string, unknown>) => unknown)({
287
- provider: 'system',
288
- sessionId: run.id,
289
- kind: 'action_required',
290
- code: 'permission.required',
291
- meta: {
292
- toolName: decision.capabilities.join(', '),
293
- sessionName: workflowNotificationTitle(run),
294
- },
295
- severity: 'warning',
296
- requiresUserAction: true,
297
- dedupeKey: `workflow:permission:${run.id}:${decision.requestId}`,
298
- });
299
- (notifyUserIfEnabled as (payload: { userId: string | number; event: unknown }) => void)({
300
- userId,
301
- event,
302
- });
303
- }
304
-
305
- function readBoolean(value: unknown): boolean | undefined {
306
- return typeof value === 'boolean' ? value : undefined;
307
- }
308
-
309
- function modelValueSet(models: ProviderModel[]): Set<string> {
310
- return new Set(models.map((model) => model.value).filter(Boolean));
311
- }
312
-
313
- function preferredFallbackModel(models: ProviderModel[], defaultModel?: string): string | undefined {
314
- const values = modelValueSet(models);
315
- if (defaultModel && values.has(defaultModel)) return defaultModel;
316
- return models.find((model) => model.source === 'api' && model.free)?.value
317
- ?? models.find((model) => model.source === 'api')?.value
318
- ?? models.find((model) => model.free)?.value
319
- ?? models[0]?.value
320
- ?? defaultModel;
321
- }
322
-
323
- async function resolveWorkflowModel(adapterId: string, requestedModel?: string): Promise<string | undefined> {
324
- const provider = adapterProviderMap[adapterId];
325
- if (!provider) return requestedModel;
326
-
327
- const defaultModel = getDefaultProviderModel(provider);
328
- if (!requestedModel) return defaultModel;
329
-
330
- try {
331
- const result = await getProviderModelRegistryEntry(provider);
332
- const models = Array.isArray(result?.models) ? result.models as ProviderModel[] : [];
333
- if (modelValueSet(models).has(requestedModel)) {
334
- return requestedModel;
335
- }
336
- return preferredFallbackModel(models, defaultModel) ?? requestedModel;
337
- } catch {
338
- const staticModels = getStaticProviderModels(provider) as ProviderModel[];
339
- const staticValues = modelValueSet(staticModels);
340
- return staticValues.has(requestedModel)
341
- ? requestedModel
342
- : preferredFallbackModel(staticModels, defaultModel) ?? requestedModel;
343
- }
344
- }
345
-
346
- function readIsolation(value: unknown): 'host' | 'worktree' | 'docker' | undefined {
347
- return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
348
- }
349
-
350
- function readLegacyEnabledAdapters(metadata?: Record<string, unknown>): string[] {
351
- return Array.isArray(metadata?.enabledAdapters)
352
- ? metadata.enabledAdapters.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
353
- : [];
354
- }
355
-
356
- function readMetadataAgents(metadata?: Record<string, unknown>): AgentAssignment[] {
357
- if (!Array.isArray(metadata?.agents)) return [];
358
-
359
- return metadata.agents
360
- .map((value, index): AgentAssignment | null => {
361
- if (!value || typeof value !== 'object') return null;
362
- const record = value as Record<string, unknown>;
363
- const adapterId = readString(record.adapterId);
364
- if (!adapterId) return null;
365
- if (readBoolean(record.enabled) === false) return null;
366
-
367
- return {
368
- instanceId: readString(record.instanceId) ?? `${adapterId}-${index + 1}`,
369
- adapterId,
370
- label: readString(record.label) ?? `${adapterId} #${index + 1}`,
371
- role: readAgentRole(record.role),
372
- instruction: readString(record.instruction),
373
- model: readString(record.model),
374
- permissionMode: readString(record.permissionMode),
375
- toolsSettings: readRecord(record.toolsSettings),
376
- order: index,
377
- };
378
- })
379
- .filter((value): value is AgentAssignment => Boolean(value))
380
- .slice(0, 16);
381
- }
382
-
383
- function readAgentAssignments(metadata?: Record<string, unknown>): AgentAssignment[] {
384
- const agents = readMetadataAgents(metadata);
385
- if (agents.length > 0) return agents;
386
-
387
- return readLegacyEnabledAdapters(metadata).map((adapterId, index) => ({
388
- instanceId: `${adapterId}-${index + 1}`,
389
- adapterId,
390
- label: `${adapterId} #${index + 1}`,
391
- order: index,
392
- }));
393
- }
394
-
395
- function readEnabledAdapters(metadata?: Record<string, unknown>): string[] {
396
- return [...new Set(readAgentAssignments(metadata).map((agent) => agent.adapterId))];
397
- }
398
-
399
- function readMaxParallelAgents(metadata?: Record<string, unknown>): number {
400
- const settings = getMetadataRecord(metadata, 'settings');
401
- const value = settings.maxParallelAgents;
402
- return typeof value === 'number' && Number.isFinite(value)
403
- ? Math.max(1, Math.min(12, Math.round(value)))
404
- : 3;
405
- }
406
-
407
- function readMaxRepairCycles(metadata?: Record<string, unknown>): number {
408
- const settings = getMetadataRecord(metadata, 'settings');
409
- const value = settings.maxRepairCycles;
410
- return typeof value === 'number' && Number.isFinite(value)
411
- ? Math.max(0, Math.min(MAX_REPAIR_CYCLES, Math.round(value)))
412
- : DEFAULT_MAX_REPAIR_CYCLES;
413
- }
414
-
415
- function safeNodeId(adapterId: string, suffix: string): string {
416
- return `${adapterId.replace(/[^a-zA-Z0-9_]+/g, '_')}_${suffix}`;
417
- }
418
-
419
- function safeAgentNodeId(agent: AgentAssignment, index: number, suffix: string): string {
420
- return `agent_${index + 1}_${safeNodeId(agent.adapterId, suffix)}`;
421
- }
422
-
423
- function agentRoster(agents: AgentAssignment[]): string {
424
- return agents
425
- .map((agent, index) => {
426
- const instruction = agent.instruction
427
- ? `\n User assignment: ${agent.instruction}`
428
- : '';
429
- const role = agent.role ? `\n API role: ${agent.role}` : '';
430
- return `${index + 1}. ${agent.label} (${agent.adapterId})${role}${instruction}`;
431
- })
432
- .join('\n');
433
- }
434
-
435
- function inferAgentRole(agent: AgentAssignment): AgentRole {
436
- if (isKnownAgentRole(agent.role)) return agent.role;
437
-
438
- const text = `${agent.label} ${agent.adapterId} ${agent.role ?? ''} ${agent.instruction ?? ''}`.toLocaleLowerCase('tr');
439
- if (/(test|tester|qa|review|code review|hata|kontrol|onay|incele|doğrula|dogrula)/u.test(text)) {
440
- return 'review';
441
- }
442
- if (/(backend|back-end|api|server|veri|database|db|fapi|endpoint|websocket|ws)/u.test(text)) {
443
- return 'backend';
444
- }
445
- if (/(frontend|front-end|ui|ux|tailwind|tasarım|tasarim|design|chart|tradingview|arayüz|arayuz)/u.test(text)) {
446
- return 'frontend';
447
- }
448
- return 'implementation';
449
- }
450
-
451
- function inferImplementationRole(agent: AgentAssignment): 'backend' | 'frontend' | 'review' | 'implementation' {
452
- const role = inferAgentRole(agent);
453
- return role === 'backend' || role === 'frontend' || role === 'review' || role === 'implementation'
454
- ? role
455
- : 'implementation';
456
- }
457
-
458
- function displayStage(agent: AgentAssignment, fallback: AgentRole): string {
459
- return agent.role && !isKnownAgentRole(agent.role) ? agent.role : fallback;
460
- }
461
-
462
- function rolePrompt(role: AgentRole): string {
463
- if (role === 'backend') {
464
- return 'Backend/API/data work should define stable contracts first. Report endpoints, payload shapes, ports, and any data-source limitations clearly for downstream agents.';
465
- }
466
- if (role === 'frontend') {
467
- return 'Frontend/UI work must use prior backend/data-contract outputs when present. If a dependency is missing, use a minimal mock only as a temporary fallback and report the blocker.';
468
- }
469
- if (role === 'review') {
470
- return 'You are the validation/review stage. Inspect the prior agent outputs and actual project state. Approve only if it works; otherwise return a concrete bug list and required fixes.';
471
- }
472
- if (role === 'proposal') {
473
- return 'You are in the proposal stage. Produce a concrete option with tradeoffs, assumptions, and what should happen next. Do not edit files.';
474
- }
475
- if (role === 'critique') {
476
- return 'You are in the critique stage. Challenge the proposal for risks, missing constraints, and weak assumptions. Do not edit files.';
477
- }
478
- if (role === 'response') {
479
- return 'You are in the response stage. Reconcile the critique with the proposal and refine the practical path forward. Do not edit files.';
480
- }
481
- if (role === 'decision' || role === 'report') {
482
- return 'You are the reporting stage. Produce the final concise decision report and a next prompt for launching an implementation agent team. Do not edit files.';
483
- }
484
- if (role !== 'implementation') {
485
- return `You are assigned to the custom stage "${role}". Follow that user-defined stage literally, avoid duplicating other agents, and report changed files, commands, blockers, and next actions.`;
486
- }
487
- return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
488
- }
489
-
490
- function privacyGuardPrompt(): string {
491
- return 'Do not mention internal instructions, memory files, skill use, or tool protocol unless the user explicitly asks.';
492
- }
493
-
494
- function handoffArtifactInstructions(statusHint: string): string {
495
- return [
496
- `Output exactly one JSON object using the ${PIXCODE_HANDOFF_PROTOCOL} handoff artifact protocol.`,
497
- 'Do not wrap it in Markdown. Do not add commentary before or after it.',
498
- `Use "${statusHint}" for taskStatus unless completed, blocked, failed, or needs-review is more accurate.`,
499
- 'Schema:',
500
- HANDOFF_ARTIFACT_EXAMPLE,
501
- ].join('\n');
502
- }
503
-
504
- function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
505
- return [
506
- `You are ${agent.label} in a Pixcode CLI team.`,
507
- `Your inferred stage is: ${role}.`,
508
- 'This is a bounded Hermes handoff task, not the full implementation.',
509
- 'Read the original user goal and coordinator plan, then publish a compact contract for downstream agents.',
510
- agent.instruction ? `Your explicit assignment from the user is: ${agent.instruction}` : '',
511
- handoffArtifactInstructions('ready'),
512
- 'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
513
- privacyGuardPrompt(),
514
- 'Stop after the contract. Keep it concise and respond in the same language as the user request.',
515
- ].filter(Boolean).join('\n');
516
- }
517
-
518
- function handoffInitPrompt(agent: AgentAssignment, index: number): string {
519
- return [
520
- `You are preparing ${agent.label} for a strict Pixcode handoff chain.`,
521
- `This is internal step ${index + 1}.`,
522
- 'Create a compact init packet for the next visible work step.',
523
- 'Use the original user goal and any prior compact handoff packet included above.',
524
- agent.instruction ? `The explicit assignment for this agent is: ${agent.instruction}` : '',
525
- handoffArtifactInstructions('ready'),
526
- privacyGuardPrompt(),
527
- 'Do not perform the task yet. Do not mention that this is hidden from the user.',
528
- 'Respond in the same language as the user request.',
529
- ].filter(Boolean).join('\n');
530
- }
531
-
532
- function handoffWorkPrompt(agent: AgentAssignment, index: number): string {
533
- return [
534
- `You are ${agent.label} in a strict Pixcode handoff chain.`,
535
- `This is visible work step ${index + 1}.`,
536
- 'The internal init packet above is your starting context. Do the assigned work now.',
537
- agent.instruction
538
- ? `Your explicit assignment from the user is: ${agent.instruction}`
539
- : 'Use the init packet and original user goal to choose the next useful work for this step.',
540
- rolePrompt(agent.role ?? 'implementation'),
541
- privacyGuardPrompt(),
542
- 'Report only user-facing progress, changed files, commands, verification, blockers, and next actions.',
543
- 'Respond in the same language as the user request.',
544
- ].filter(Boolean).join('\n');
545
- }
546
-
547
- function handoffCompactPrompt(agent: AgentAssignment, index: number): string {
548
- return [
549
- `You are compacting ${agent.label}'s strict handoff output for the next Pixcode agent.`,
550
- `This is internal compact step ${index + 1}.`,
551
- 'Read the prior visible work output included above and create a compact handoff packet.',
552
- handoffArtifactInstructions('completed'),
553
- privacyGuardPrompt(),
554
- 'Do not include raw logs unless they are essential. Keep it concise and actionable.',
555
- 'Respond in the same language as the user request.',
556
- ].join('\n');
557
- }
558
-
559
- function compactOutputForContext(text: string): string {
560
- if (text.length <= MAX_OUTPUT_CONTEXT_CHARS) {
561
- return text;
562
- }
563
-
564
- const edge = Math.floor(MAX_OUTPUT_CONTEXT_CHARS / 2);
565
- return [
566
- text.slice(0, edge),
567
- `\n\n[...${text.length - MAX_OUTPUT_CONTEXT_CHARS} characters omitted from prior agent output...]\n\n`,
568
- text.slice(-edge),
569
- ].join('');
570
- }
571
-
572
- function requiresHandoffArtifact(node: WorkflowNode): boolean {
573
- return node.stage === 'handoff' || node.stage === 'handoff_init' || node.stage === 'handoff_compact';
574
- }
575
-
576
- function handoffArtifactSource(result: TaskResult): string {
577
- const structured = result.artifacts.find((artifact) => artifact.type === 'handoff-artifact' && artifact.data);
578
- if (structured?.data) {
579
- return JSON.stringify(structured.data);
580
- }
581
- return result.text;
582
- }
583
-
584
- function isExternalDirectoryPermissionError(value: unknown): boolean {
585
- const text = String(value ?? '').toLocaleLowerCase('en');
586
- return (
587
- text.includes('external_directory') ||
588
- /permission requested:.*auto-rejecting/u.test(text) ||
589
- /auto-rejecting.*permission/u.test(text) ||
590
- /outside (the )?(workspace|working directory)/u.test(text) ||
591
- /permission.*external/u.test(text)
592
- );
593
- }
594
-
595
- function isFinalReportNode(node: WorkflowNode): boolean {
596
- return node.id === 'final_report' || node.stage === 'final_report' || node.stage === 'report';
597
- }
598
-
599
- function workspaceNeedsHostPermissionBypass(target: ResolvedWorkspaceTarget): boolean {
600
- return (target.kind === 'selected_project' || target.kind === 'custom') && target.projectPath !== target.appRoot;
601
- }
602
-
603
- function resolveNodePermissionMode(node: WorkflowNode, target: ResolvedWorkspaceTarget): string | undefined {
604
- if (node.permissionMode && node.permissionMode !== 'default') {
605
- return node.permissionMode;
606
- }
607
-
608
- if (workspaceNeedsHostPermissionBypass(target)) {
609
- return 'bypassPermissions';
610
- }
611
-
612
- return node.permissionMode;
613
- }
614
-
615
- function buildPermissionFallbackOutput(
616
- node: WorkflowNode,
617
- reason: string,
618
- target: ResolvedWorkspaceTarget,
619
- ): string {
620
- return [
621
- 'Bu adım çalışma alanı izin sınırına takıldı.',
622
- '',
623
- `Ajan: ${node.agentLabel || node.id}`,
624
- `Hedef çalışma alanı: ${target.projectPath}`,
625
- `Hata: ${reason}`,
626
- '',
627
- 'Pixcode bu adımı workflow dışına taşırmadan devam ettirdi. Ajan aynı dış dizin yoluna tekrar tekrar erişmek yerine mevcut bağlamla ilerlemeli.',
628
- ].join('\n');
629
- }
630
-
631
- function buildFallbackFinalReport(
632
- outputs: Map<string, string>,
633
- reason: string,
634
- target: ResolvedWorkspaceTarget,
635
- ): string {
636
- const completedOutputs = [...outputs.entries()]
637
- .map(([nodeId, output]) => [`## ${nodeId}`, output || '(çıktı yok)'].join('\n'))
638
- .join('\n\n');
639
-
640
- return [
641
- 'Final rapor aracı çalışma alanı izin sınırına takıldı, bu yüzden Pixcode tamamlanan ajan çıktılarından güvenli bir özet üretti.',
642
- '',
643
- `Hedef çalışma alanı: ${target.projectPath}`,
644
- `İzin hatası: ${reason}`,
645
- '',
646
- completedOutputs || 'Bu turda final rapora aktarılabilecek tamamlanmış ajan çıktısı yok.',
647
- ].join('\n');
648
- }
649
-
650
- function completeNodeWithPermissionFallback(
651
- nodeRun: WorkflowNodeRun,
652
- node: WorkflowNode,
653
- outputs: Map<string, string>,
654
- completed: Set<string>,
655
- reason: string,
656
- target: ResolvedWorkspaceTarget,
657
- ): void {
658
- const outputText = isFinalReportNode(node)
659
- ? buildFallbackFinalReport(outputs, reason, target)
660
- : buildPermissionFallbackOutput(node, reason, target);
661
-
662
- nodeRun.status = 'completed';
663
- nodeRun.error = reason;
664
- nodeRun.outputText = outputText;
665
- nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
666
- outputs.set(node.id, compactOutputForContext(outputText));
667
- completed.add(node.id);
668
- }
669
-
670
- function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
671
- const agents = readAgentAssignments(metadata);
672
- if (agents.length === 0) {
673
- throw new Error('Select at least one CLI agent.');
674
- }
675
-
676
- const coordinator = agents.find((agent) => agent.adapterId === 'claude-code') ?? agents[0];
677
- const roster = agentRoster(agents);
678
- const workerSpecs = agents.map((agent, index) => ({
679
- agent,
680
- role: inferImplementationRole(agent),
681
- stage: displayStage(agent, inferImplementationRole(agent)),
682
- nodeId: safeAgentNodeId(agent, index, 'work'),
683
- handoffNodeId: safeAgentNodeId(agent, index, 'handoff'),
684
- }));
685
- const backendHandoffNodeIds = workerSpecs
686
- .filter((spec) => spec.role === 'backend')
687
- .map((spec) => spec.handoffNodeId);
688
- const implementationNodeIds = workerSpecs
689
- .filter((spec) => spec.role !== 'review')
690
- .map((spec) => spec.nodeId);
691
- const handoffNodes: WorkflowNode[] = workerSpecs
692
- .filter((spec) => spec.role === 'backend')
693
- .map(({ agent, role, handoffNodeId }) => ({
694
- id: handoffNodeId,
695
- adapterId: agent.adapterId,
696
- agentInstanceId: agent.instanceId,
697
- agentLabel: `${agent.label} Handoff`,
698
- assignment: agent.instruction,
699
- stage: 'handoff',
700
- model: agent.model,
701
- permissionMode: agent.permissionMode,
702
- toolsSettings: agent.toolsSettings,
703
- prompt: handoffPrompt(agent, role),
704
- inputs: ['coordinator'],
705
- output: 'message',
706
- onFail: 'continue',
707
- timeoutMs: BACKEND_HANDOFF_TIMEOUT_MS,
708
- }));
709
- const workerNodes: WorkflowNode[] = workerSpecs.map(({ agent, role, stage, nodeId, handoffNodeId }) => {
710
- const inputs = role === 'review'
711
- ? (implementationNodeIds.length > 0 ? implementationNodeIds : ['coordinator'])
712
- : role === 'frontend' && backendHandoffNodeIds.length > 0
713
- ? ['coordinator', ...backendHandoffNodeIds]
714
- : role === 'backend'
715
- ? ['coordinator', handoffNodeId]
716
- : ['coordinator'];
717
-
718
- return {
719
- id: nodeId,
720
- adapterId: agent.adapterId,
721
- agentInstanceId: agent.instanceId,
722
- agentLabel: agent.label,
723
- assignment: agent.instruction,
724
- stage,
725
- model: agent.model,
726
- permissionMode: agent.permissionMode,
727
- toolsSettings: agent.toolsSettings,
728
- prompt: [
729
- `You are ${agent.label} in a Pixcode CLI team.`,
730
- `Your stage is: ${stage}.`,
731
- stage !== role ? `Runtime routing category: ${role}.` : '',
732
- 'The coordinator plan and any dependency outputs are included above. Use them together with the original user goal.',
733
- agent.instruction
734
- ? `Your explicit assignment from the user is: ${agent.instruction}`
735
- : 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
736
- rolePrompt(stage),
737
- privacyGuardPrompt(),
738
- 'Respond in the same language as the user request.',
739
- ].filter(Boolean).join('\n'),
740
- inputs,
741
- output: 'both',
742
- onFail: 'continue',
743
- };
744
- });
745
-
746
- return {
747
- ...workflow,
748
- nodes: [
749
- {
750
- id: 'coordinator',
751
- adapterId: coordinator.adapterId,
752
- agentInstanceId: coordinator.instanceId,
753
- agentLabel: coordinator.label,
754
- stage: 'coordinator',
755
- model: coordinator.model,
756
- permissionMode: coordinator.permissionMode,
757
- toolsSettings: coordinator.toolsSettings,
758
- prompt: [
759
- 'You are the coordinator for a Pixcode CLI agent team.',
760
- 'Read the user goal, active CLI roster, and any per-agent assignments. Create a compact execution plan for the selected agents.',
761
- 'If the user directly names a CLI, honor that. Do not invent permanent roles; assign work only from the goal, active agents, and explicit assignment text.',
762
- `Active roster:\n${roster}`,
763
- 'Respond in the same language as the user request.',
764
- ].join('\n'),
765
- inputs: [],
766
- output: 'message',
767
- onFail: 'abort',
768
- },
769
- ...handoffNodes,
770
- ...workerNodes,
771
- {
772
- id: 'final_report',
773
- adapterId: coordinator.adapterId,
774
- agentInstanceId: coordinator.instanceId,
775
- agentLabel: coordinator.label,
776
- stage: 'final_report',
777
- model: coordinator.model,
778
- permissionMode: coordinator.permissionMode,
779
- toolsSettings: coordinator.toolsSettings,
780
- prompt: [
781
- 'Collect the worker outputs into one user-facing result.',
782
- 'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
783
- 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
784
- 'If a worker reveals internal process text, summarize only the useful user-facing result.',
785
- 'Respond in the same language as the user request.',
786
- ].join('\n'),
787
- inputs: workerNodes.map((node) => node.id),
788
- output: 'message',
789
- onFail: 'abort',
790
- },
791
- ],
792
- };
793
- }
794
-
795
- function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
796
- return [
797
- `You are ${agent.label} in a Pixcode decision workflow.`,
798
- `Your stage is: ${stage}.`,
799
- agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
800
- agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
801
- rolePrompt(stage),
802
- privacyGuardPrompt(),
803
- 'Keep the answer concise, structured, and useful for the next stage.',
804
- 'Respond in the same language as the user request.',
805
- ].filter(Boolean).join('\n');
806
- }
807
-
808
- function agentsWithRole(agents: AgentAssignment[], role: AgentRole): AgentAssignment[] {
809
- return agents.filter((agent) => agent.role === role);
810
- }
811
-
812
- function autoAssignDebateAgents(agents: AgentAssignment[]): {
813
- proposalAgents: AgentAssignment[];
814
- critiqueAgents: AgentAssignment[];
815
- responseAgents: AgentAssignment[];
816
- reportAgent: AgentAssignment;
817
- } {
818
- const assigned = new Set<string>();
819
- const markAssigned = (items: AgentAssignment[]) => {
820
- for (const item of items) assigned.add(item.instanceId);
821
- };
822
- const pickNext = () =>
823
- agents.find((agent) => !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report')
824
- ?? agents.find((agent) => !assigned.has(agent.instanceId))
825
- ?? agents[0];
826
-
827
- const proposalAgents = agentsWithRole(agents, 'proposal');
828
- if (proposalAgents.length === 0) proposalAgents.push(pickNext());
829
- markAssigned(proposalAgents);
830
-
831
- const critiqueAgents = agentsWithRole(agents, 'critique');
832
- if (critiqueAgents.length === 0) critiqueAgents.push(pickNext());
833
- markAssigned(critiqueAgents);
834
-
835
- const responseAgents = agentsWithRole(agents, 'response');
836
- if (responseAgents.length === 0 && agents.length > 2) {
837
- responseAgents.push(...agents.filter((agent) =>
838
- !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report',
839
- ));
840
- }
841
- markAssigned(responseAgents);
842
-
843
- const reportAgent = agentsWithRole(agents, 'decision')[0]
844
- ?? agentsWithRole(agents, 'report')[0]
845
- ?? agents[0];
846
-
847
- return { proposalAgents, critiqueAgents, responseAgents, reportAgent };
848
- }
849
-
850
- function expandAdversarialDebateWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
851
- const agents = readAgentAssignments(metadata);
852
- if (agents.length === 0) {
853
- throw new Error('Select at least one CLI agent.');
854
- }
855
-
856
- const {
857
- proposalAgents,
858
- critiqueAgents,
859
- responseAgents,
860
- reportAgent,
861
- } = autoAssignDebateAgents(agents);
862
-
863
- const proposalNodes: WorkflowNode[] = proposalAgents.map((agent, index) => ({
864
- id: safeAgentNodeId(agent, index, 'proposal'),
865
- adapterId: agent.adapterId,
866
- agentInstanceId: agent.instanceId,
867
- agentLabel: agent.label,
868
- assignment: agent.instruction || 'Proposal stage',
869
- stage: 'proposal',
870
- model: agent.model,
871
- permissionMode: agent.permissionMode,
872
- toolsSettings: agent.toolsSettings,
873
- prompt: stagePrompt(agent, 'proposal'),
874
- inputs: [],
875
- output: 'message',
876
- onFail: 'continue',
877
- }));
878
- const critiqueNodes: WorkflowNode[] = critiqueAgents.map((agent, index) => ({
879
- id: safeAgentNodeId(agent, index, 'critique'),
880
- adapterId: agent.adapterId,
881
- agentInstanceId: agent.instanceId,
882
- agentLabel: agent.label,
883
- assignment: agent.instruction || 'Critique stage',
884
- stage: 'critique',
885
- model: agent.model,
886
- permissionMode: agent.permissionMode,
887
- toolsSettings: agent.toolsSettings,
888
- prompt: stagePrompt(agent, 'critique'),
889
- inputs: proposalNodes.map((node) => node.id),
890
- output: 'message',
891
- onFail: 'continue',
892
- }));
893
- const responseNodes: WorkflowNode[] = responseAgents.map((agent, index) => ({
894
- id: safeAgentNodeId(agent, index, 'response'),
895
- adapterId: agent.adapterId,
896
- agentInstanceId: agent.instanceId,
897
- agentLabel: agent.label,
898
- assignment: agent.instruction || 'Response stage',
899
- stage: 'response',
900
- model: agent.model,
901
- permissionMode: agent.permissionMode,
902
- toolsSettings: agent.toolsSettings,
903
- prompt: stagePrompt(agent, 'response'),
904
- inputs: critiqueNodes.map((node) => node.id),
905
- output: 'message',
906
- onFail: 'continue',
907
- }));
908
- const finalInputs = responseNodes.length > 0
909
- ? responseNodes.map((node) => node.id)
910
- : critiqueNodes.map((node) => node.id);
911
-
912
- return {
913
- ...workflow,
914
- nodes: [
915
- ...proposalNodes,
916
- ...critiqueNodes,
917
- ...responseNodes,
918
- {
919
- id: 'final_report',
920
- adapterId: reportAgent.adapterId,
921
- agentInstanceId: reportAgent.instanceId,
922
- agentLabel: reportAgent.label,
923
- assignment: reportAgent.instruction || 'Final decision report',
924
- stage: 'final_report',
925
- model: reportAgent.model,
926
- permissionMode: reportAgent.permissionMode,
927
- toolsSettings: reportAgent.toolsSettings,
928
- prompt: [
929
- 'Produce the final decision report from the debate.',
930
- 'Use this exact structure:',
931
- '1. Short decision',
932
- '2. Why',
933
- '3. Risks',
934
- '4. Suggested next prompt',
935
- '5. Proposed agent team and assignments',
936
- 'The next prompt should be ready to paste into Pixcode Agent Team mode.',
937
- 'Do not edit files. Respond in the same language as the user request.',
938
- ].join('\n'),
939
- inputs: finalInputs,
940
- output: 'message',
941
- onFail: 'abort',
942
- },
943
- ],
944
- };
945
- }
946
-
947
- function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
948
- const agents = readAgentAssignments(metadata);
949
- if (agents.length === 0) {
950
- throw new Error('Select at least one CLI agent.');
951
- }
952
-
953
- const nodes: WorkflowNode[] = agents.flatMap((agent, index): WorkflowNode[] => {
954
- const initNodeId = safeAgentNodeId(agent, index, 'init');
955
- const workNodeId = safeAgentNodeId(agent, index, 'work');
956
- const compactNodeId = safeAgentNodeId(agent, index, 'compact');
957
-
958
- return [
959
- {
960
- id: initNodeId,
961
- adapterId: agent.adapterId,
962
- agentInstanceId: agent.instanceId,
963
- agentLabel: `${agent.label} Init`,
964
- assignment: agent.instruction,
965
- stage: 'handoff_init',
966
- model: agent.model,
967
- permissionMode: agent.permissionMode,
968
- toolsSettings: agent.toolsSettings,
969
- prompt: handoffInitPrompt(agent, index),
970
- inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'compact')],
971
- output: 'message',
972
- onFail: 'abort',
973
- internal: true,
974
- },
975
- {
976
- id: workNodeId,
977
- adapterId: agent.adapterId,
978
- agentInstanceId: agent.instanceId,
979
- agentLabel: agent.label,
980
- assignment: agent.instruction,
981
- stage: agent.role ?? 'implementation',
982
- model: agent.model,
983
- permissionMode: agent.permissionMode,
984
- toolsSettings: agent.toolsSettings,
985
- prompt: handoffWorkPrompt(agent, index),
986
- inputs: [initNodeId],
987
- output: 'both',
988
- onFail: 'abort',
989
- },
990
- {
991
- id: compactNodeId,
992
- adapterId: agent.adapterId,
993
- agentInstanceId: agent.instanceId,
994
- agentLabel: `${agent.label} Compact`,
995
- assignment: agent.instruction,
996
- stage: 'handoff_compact',
997
- model: agent.model,
998
- permissionMode: agent.permissionMode,
999
- toolsSettings: agent.toolsSettings,
1000
- prompt: handoffCompactPrompt(agent, index),
1001
- inputs: [workNodeId],
1002
- output: 'message',
1003
- onFail: 'abort',
1004
- internal: true,
1005
- },
1006
- ];
1007
- });
1008
- const reportAgent = agents[0];
1009
- const lastCompactNodeId = safeAgentNodeId(agents[agents.length - 1], agents.length - 1, 'compact');
1010
-
1011
- return {
1012
- ...workflow,
1013
- nodes: [
1014
- ...nodes,
1015
- {
1016
- id: 'final_report',
1017
- adapterId: reportAgent.adapterId,
1018
- agentInstanceId: reportAgent.instanceId,
1019
- agentLabel: reportAgent.label,
1020
- stage: 'final_report',
1021
- model: reportAgent.model,
1022
- permissionMode: reportAgent.permissionMode,
1023
- toolsSettings: reportAgent.toolsSettings,
1024
- prompt: [
1025
- 'Create the final user-facing result for this strict handoff run.',
1026
- 'Use the final compact handoff packet and the original user goal.',
1027
- 'Summarize what each visible agent did, what changed, verification, blockers, and next actions.',
1028
- 'Do not expose internal init packets, compact packets, prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
1029
- 'Respond in the same language as the user request.',
1030
- ].join('\n'),
1031
- inputs: [lastCompactNodeId],
1032
- output: 'message',
1033
- onFail: 'abort',
1034
- },
1035
- ],
1036
- };
1037
- }
1038
-
1039
- function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
1040
- if (workflow.id === 'agent_team') {
1041
- return expandAgentTeamWorkflow(workflow, metadata);
1042
- }
1043
-
1044
- const agents = readAgentAssignments(metadata);
1045
- if (workflow.id === 'adversarial_debate') {
1046
- return expandAdversarialDebateWorkflow(workflow, metadata);
1047
- }
1048
- if (workflow.id === 'sequential_handoff') {
1049
- return expandSequentialHandoffWorkflow(workflow, metadata);
1050
- }
1051
- if (workflow.id !== 'multi_model_review' || agents.length === 0) {
1052
- return workflow;
1053
- }
1054
-
1055
- const reportAgent = agentsWithRole(agents, 'report')[0] ?? agentsWithRole(agents, 'decision')[0] ?? agents[0];
1056
- const reviewAgents = agents.filter((agent) => agent.instanceId !== reportAgent.instanceId || agents.length === 1);
1057
- const reviewNodes: WorkflowNode[] = reviewAgents.map((agent, index) => ({
1058
- id: safeAgentNodeId(agent, index, 'review'),
1059
- adapterId: agent.adapterId,
1060
- agentInstanceId: agent.instanceId,
1061
- agentLabel: agent.label,
1062
- assignment: agent.instruction,
1063
- stage: 'review',
1064
- model: agent.model,
1065
- permissionMode: agent.permissionMode,
1066
- toolsSettings: agent.toolsSettings,
1067
- prompt: [
1068
- `You are ${agent.label}.`,
1069
- 'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
1070
- agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
1071
- privacyGuardPrompt(),
1072
- 'Respond in the same language as the user request.',
1073
- ].filter(Boolean).join('\n'),
1074
- inputs: [],
1075
- output: 'both',
1076
- onFail: 'continue',
1077
- }));
1078
-
1079
- return {
1080
- ...workflow,
1081
- nodes: [
1082
- ...reviewNodes,
1083
- {
1084
- id: 'aggregate',
1085
- adapterId: reportAgent.adapterId,
1086
- agentInstanceId: reportAgent.instanceId,
1087
- agentLabel: reportAgent.label,
1088
- stage: 'report',
1089
- model: reportAgent.model,
1090
- permissionMode: reportAgent.permissionMode,
1091
- toolsSettings: reportAgent.toolsSettings,
1092
- prompt: [
1093
- 'Aggregate the prior agent reviews into a concise prioritized report.',
1094
- 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
1095
- 'Respond in the same language as the user request.',
1096
- ].join('\n'),
1097
- inputs: reviewNodes.map((node) => node.id),
1098
- output: 'message',
1099
- onFail: 'abort',
1100
- },
1101
- ],
1102
- };
1103
- }
1104
-
1105
- async function cancelHermesTask(taskId: string): Promise<void> {
1106
- await fetch(`${localHermesBaseUrl()}/tasks/${taskId}/cancel`, { method: 'POST' }).catch(() => undefined);
1107
- }
1108
-
1109
- function readTaskResult(task: RawTask): TaskResult {
1110
- const messages = (task.history ?? []).map((message) => ({
1111
- role: typeof message.role === 'string' ? message.role : 'agent',
1112
- text: (message.parts ?? [])
1113
- .filter((part) => part.kind === 'text' && typeof part.text === 'string')
1114
- .map((part) => part.text)
1115
- .join('\n'),
1116
- })).filter((message) => message.text.trim());
1117
- const artifacts = (task.artifacts ?? []).map((artifact) => {
1118
- const text = (artifact.parts ?? [])
1119
- .filter((part) => part.kind === 'text' && typeof part.text === 'string')
1120
- .map((part) => part.text)
1121
- .join('\n');
1122
- const data = (artifact.parts ?? []).find((part) => part.kind === 'data')?.data;
1123
- return {
1124
- type: artifact.type ?? 'data',
1125
- text: text || undefined,
1126
- data,
1127
- metadata: artifact.metadata,
1128
- };
1129
- });
1130
- const outputMessages = messages.filter((message) => message.role !== 'user');
1131
- const userFacingTaskText = outputMessages.map((message) => message.text.trim()).filter(Boolean).join('\n\n');
1132
- const error = task.error?.message
1133
- ? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
1134
- : undefined;
1135
- return {
1136
- state: task.state ?? 'submitted',
1137
- text: userFacingTaskText,
1138
- error,
1139
- messages,
1140
- artifacts,
1141
- };
1142
- }
1143
-
1144
- async function waitForTask(
1145
- taskId: string,
1146
- shouldCancel?: () => boolean,
1147
- onSnapshot?: (result: TaskResult) => void,
1148
- timeoutMs?: number,
1149
- ): Promise<TaskResult> {
1150
- const timeout = timeoutMs && timeoutMs > 0 ? timeoutMs : undefined;
1151
- const deadline = timeout ? Date.now() + timeout : undefined;
1152
- for (;;) {
1153
- if (shouldCancel?.()) {
1154
- throw new WorkflowCanceledError();
1155
- }
1156
- if (deadline && Date.now() >= deadline) {
1157
- throw new WorkflowNodeTimeoutError(timeout ?? 0);
1158
- }
1159
- const response = await fetch(`${localHermesBaseUrl()}/tasks/${taskId}`);
1160
- const task = await response.json() as RawTask;
1161
- const snapshot = readTaskResult(task);
1162
- onSnapshot?.(snapshot);
1163
- if (task.state && TERMINAL.has(task.state)) {
1164
- return snapshot;
1165
- }
1166
- await new Promise((resolve) => setTimeout(resolve, 1000));
1167
- }
1168
- }
1169
-
1170
- function readyNodes(workflow: Workflow, completed: Set<string>, started: Set<string>): WorkflowNode[] {
1171
- return workflow.nodes.filter((node) =>
1172
- !started.has(node.id) && node.inputs.every((input) => completed.has(input)),
1173
- );
1174
- }
1175
-
1176
- function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
1177
- return {
1178
- nodeId: node.id,
1179
- adapterId: node.adapterId,
1180
- agentInstanceId: node.agentInstanceId,
1181
- agentLabel: node.agentLabel,
1182
- assignment: node.assignment,
1183
- promptPreview: node.prompt,
1184
- model: node.model,
1185
- permissionMode: node.permissionMode,
1186
- timeoutMs: node.timeoutMs,
1187
- stage: node.stage,
1188
- internal: node.internal,
1189
- fallbackTrigger: node.fallbackTrigger,
1190
- fallbackSourceNodeId: node.fallbackSourceNodeId,
1191
- status: 'queued',
1192
- };
1193
- }
1194
-
1195
- function uniqueInputs(inputs: string[]): string[] {
1196
- return [...new Set(inputs.filter(Boolean))];
1197
- }
1198
-
1199
- function isReviewNode(node: WorkflowNode): boolean {
1200
- return node.stage === 'review';
1201
- }
1202
-
1203
- function isImplementationNode(node: WorkflowNode): boolean {
1204
- return node.stage === 'backend' || node.stage === 'frontend' || node.stage === 'implementation' || node.stage === 'repair';
1205
- }
1206
-
1207
- function reviewRequiresRepair(text: string): boolean {
1208
- const normalized = text.toLocaleLowerCase('tr').replace(/\s+/g, ' ').trim();
1209
- if (!normalized) return false;
1210
-
1211
- const approvalPatterns = [
1212
- /hata yok/u,
1213
- /sorun yok/u,
1214
- /problem yok/u,
1215
- /bulgu yok/u,
1216
- /kritik bulgu yok/u,
1217
- /temiz/u,
1218
- /onaylı/u,
1219
- /onayli/u,
1220
- /approved/u,
1221
- /lgtm/u,
1222
- /no issues/u,
1223
- /no findings/u,
1224
- /looks good/u,
1225
- /pass(?:ed)?/u,
1226
- ];
1227
- const actionableText = approvalPatterns.reduce((current, pattern) => current.replace(pattern, ' '), normalized);
1228
- const issuePatterns = [
1229
- /hata/u,
1230
- /bug/u,
1231
- /kritik/u,
1232
- /critical/u,
1233
- /blocker/u,
1234
- /regression/u,
1235
- /failed/u,
1236
- /failure/u,
1237
- /fail/u,
1238
- /eksik/u,
1239
- /düzelt/u,
1240
- /duzelt/u,
1241
- /fix required/u,
1242
- /needs fix/u,
1243
- /sorun/u,
1244
- /risk/u,
1245
- /güvenlik/u,
1246
- /guvenlik/u,
1247
- /security/u,
1248
- /çalışmıyor/u,
1249
- /calismiyor/u,
1250
- ];
1251
-
1252
- return issuePatterns.some((pattern) => pattern.test(actionableText));
1253
- }
1254
-
1255
- function findRepairFixer(workflow: Workflow, reviewNode: WorkflowNode): WorkflowNode | undefined {
1256
- return reviewNode.inputs
1257
- .map((input) => workflow.nodes.find((node) => node.id === input))
1258
- .find((node): node is WorkflowNode => Boolean(node && isImplementationNode(node)))
1259
- ?? workflow.nodes.find((node) => isImplementationNode(node))
1260
- ?? workflow.nodes.find((node) => node.stage === 'coordinator');
1261
- }
1262
-
1263
- class WorkflowRunner {
1264
- private readonly cancelingRuns = new Set<string>();
1265
-
1266
- preview(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
1267
- const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
1268
- validateWorkflow(runtimeWorkflow);
1269
- return runtimeWorkflow;
1270
- }
1271
-
1272
- start(workflow: Workflow, input = '', metadata?: Record<string, unknown>): WorkflowRun {
1273
- const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
1274
- validateWorkflow(runtimeWorkflow);
1275
- const workspaceTarget = resolveWorkflowWorkspace(metadata);
1276
- const permissionPolicy = resolvePermissionPolicyFromMetadata(metadata);
1277
- const runMetadata: Record<string, unknown> = {
1278
- ...metadata,
1279
- permissionPolicy,
1280
- projectPath: workspaceTarget.projectPath,
1281
- selectedProjectPath: workspaceTarget.selectedProjectPath,
1282
- workspaceTarget: workspaceTargetMetadata(workspaceTarget),
1283
- };
1284
- const run: WorkflowRun = {
1285
- id: newId('wrun'),
1286
- workflowId: runtimeWorkflow.id,
1287
- contextId: newId('ctx'),
1288
- status: 'queued',
1289
- input,
1290
- nodeRuns: runtimeWorkflow.nodes.map(nodeRunFromNode),
1291
- startedAt: Date.now(),
1292
- metadata: runMetadata,
1293
- };
1294
- workflowStore.setRun(run);
1295
- const orchestrationTaskId = readString(runMetadata.orchestrationTaskId);
1296
- if (orchestrationTaskId) {
1297
- orchestrationTaskService.linkWorkflowRun(orchestrationTaskId, run);
1298
- }
1299
- void this.execute(runtimeWorkflow, run);
1300
- return run;
1301
- }
1302
-
1303
- async cancel(runId: string): Promise<WorkflowRun | undefined> {
1304
- const run = workflowStore.getRun(runId);
1305
- if (!run) return undefined;
1306
- if (TERMINAL.has(run.status)) return run;
1307
-
1308
- this.cancelingRuns.add(run.id);
1309
- const taskIds = run.nodeRuns
1310
- .filter((node) => node.hermesTaskId && (node.status === 'running' || node.status === 'queued'))
1311
- .map((node) => node.hermesTaskId as string);
1312
-
1313
- this.markCanceled(run);
1314
- workflowStore.setRun(run);
1315
-
1316
- await Promise.all(taskIds.map((taskId) => cancelHermesTask(taskId)));
1317
-
1318
- return workflowStore.getRun(run.id) ?? run;
1319
- }
1320
-
1321
- private isCanceling(runId: string): boolean {
1322
- return this.cancelingRuns.has(runId) || workflowStore.getRun(runId)?.status === 'canceled';
1323
- }
1324
-
1325
- private markCanceled(run: WorkflowRun): void {
1326
- run.status = 'canceled';
1327
- run.finishedAt = run.finishedAt ?? Date.now();
1328
- for (const nodeRun of run.nodeRuns) {
1329
- if (!TERMINAL.has(nodeRun.status) && nodeRun.status !== SKIPPED) {
1330
- nodeRun.status = 'canceled';
1331
- nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
1332
- }
1333
- }
1334
- }
1335
-
1336
- private fallbackAgentFor(run: WorkflowRun, node: WorkflowNode): AgentAssignment | undefined {
1337
- if (node.stage === 'fallback' || node.id.startsWith('fallback_')) {
1338
- return undefined;
1339
- }
1340
-
1341
- const settings = getMetadataRecord(run.metadata, 'settings');
1342
- const fallbackAgentInstanceId = readString(settings.fallbackAgentInstanceId);
1343
- if (!fallbackAgentInstanceId || fallbackAgentInstanceId === node.agentInstanceId) {
1344
- return undefined;
1345
- }
1346
-
1347
- return readAgentAssignments(run.metadata).find((agent) => agent.instanceId === fallbackAgentInstanceId);
1348
- }
1349
-
1350
- private createFallbackNode(
1351
- node: WorkflowNode,
1352
- fallbackAgent: AgentAssignment,
1353
- reason: string,
1354
- fallbackTrigger: WorkflowFallbackTrigger,
1355
- ): WorkflowNode {
1356
- const fallbackSuffix = safeNodeId(fallbackAgent.instanceId, 'fallback');
1357
- return {
1358
- ...node,
1359
- id: `fallback_${node.id}_${fallbackSuffix}`,
1360
- adapterId: fallbackAgent.adapterId,
1361
- agentInstanceId: fallbackAgent.instanceId,
1362
- agentLabel: `${fallbackAgent.label} Fallback`,
1363
- assignment: `Fallback for ${node.agentLabel || node.id}`,
1364
- stage: 'fallback',
1365
- model: fallbackAgent.model,
1366
- permissionMode: fallbackAgent.permissionMode,
1367
- toolsSettings: fallbackAgent.toolsSettings,
1368
- fallbackTrigger,
1369
- fallbackSourceNodeId: node.id,
1370
- prompt: [
1371
- 'The previous CLI agent failed on this orchestration step.',
1372
- `Failed step: ${node.agentLabel || node.id}`,
1373
- `Fallback trigger: ${fallbackTrigger}`,
1374
- `Failure: ${reason}`,
1375
- 'Take over the same assignment as the backup CLI. Use the original goal and upstream context.',
1376
- 'Do not repeat unrelated work; complete the failed step and report what you did.',
1377
- node.prompt,
1378
- ].join('\n'),
1379
- onFail: 'continue',
1380
- };
1381
- }
1382
-
1383
- private recordFallbackSkipped(
1384
- run: WorkflowRun,
1385
- node: WorkflowNode,
1386
- reason: string,
1387
- fallbackTrigger: WorkflowFallbackTrigger,
1388
- skippedReason: string,
1389
- ): void {
1390
- const fallbackSkippedEvents = Array.isArray(run.metadata?.fallbackSkippedEvents)
1391
- ? run.metadata.fallbackSkippedEvents
1392
- : [];
1393
- run.metadata = {
1394
- ...run.metadata,
1395
- fallbackSkippedEvents: [
1396
- ...fallbackSkippedEvents,
1397
- {
1398
- nodeId: node.id,
1399
- trigger: fallbackTrigger,
1400
- reason,
1401
- skippedReason,
1402
- createdAt: Date.now(),
1403
- },
1404
- ],
1405
- };
1406
- workflowStore.setRun(run);
1407
- }
1408
-
1409
- private async runFallbackAfterFailure(
1410
- node: WorkflowNode,
1411
- workflow: Workflow,
1412
- run: WorkflowRun,
1413
- outputs: Map<string, string>,
1414
- started: Set<string>,
1415
- completed: Set<string>,
1416
- reason: string,
1417
- trigger?: WorkflowFallbackTrigger,
1418
- ): Promise<boolean> {
1419
- const fallbackTrigger = classifyWorkflowFailure(reason, trigger);
1420
- const fallbackAgent = this.fallbackAgentFor(run, node);
1421
- if (!fallbackAgent) {
1422
- this.recordFallbackSkipped(run, node, reason, fallbackTrigger, 'No fallback agent is configured for this run.');
1423
- return false;
1424
- }
1425
- const decision = resolveWorkflowFallbackDecision({
1426
- run,
1427
- node,
1428
- reason,
1429
- trigger: fallbackTrigger,
1430
- fallbackAgentInstanceId: fallbackAgent.instanceId,
1431
- });
1432
- if (!decision.shouldFallback) {
1433
- this.recordFallbackSkipped(
1434
- run,
1435
- node,
1436
- reason,
1437
- decision.trigger,
1438
- decision.skippedReason ?? 'Fallback policy skipped this failure.',
1439
- );
1440
- return false;
1441
- }
1442
- if (workflow.nodes.length + 1 > 64) {
1443
- run.metadata = {
1444
- ...run.metadata,
1445
- fallbackSkipped: `Workflow node limit reached after ${node.id}.`,
1446
- };
1447
- workflowStore.setRun(run);
1448
- return false;
1449
- }
1450
-
1451
- let fallbackNode = this.createFallbackNode(node, fallbackAgent, reason, decision.trigger);
1452
- let collision = 1;
1453
- while (workflow.nodes.some((candidate) => candidate.id === fallbackNode.id)) {
1454
- collision += 1;
1455
- fallbackNode = {
1456
- ...fallbackNode,
1457
- id: `${fallbackNode.id}_${collision}`,
1458
- };
1459
- }
1460
-
1461
- const nodeIndex = workflow.nodes.findIndex((candidate) => candidate.id === node.id);
1462
- const runIndex = run.nodeRuns.findIndex((candidate) => candidate.nodeId === node.id);
1463
- if (nodeIndex >= 0) {
1464
- workflow.nodes.splice(nodeIndex + 1, 0, fallbackNode);
1465
- } else {
1466
- workflow.nodes.push(fallbackNode);
1467
- }
1468
- if (runIndex >= 0) {
1469
- run.nodeRuns.splice(runIndex + 1, 0, nodeRunFromNode(fallbackNode));
1470
- } else {
1471
- run.nodeRuns.push(nodeRunFromNode(fallbackNode));
1472
- }
1473
-
1474
- const fallbackEvents = Array.isArray(run.metadata?.fallbackEvents)
1475
- ? run.metadata.fallbackEvents
1476
- : [];
1477
- run.metadata = {
1478
- ...run.metadata,
1479
- fallbackEvents: [
1480
- ...fallbackEvents,
1481
- {
1482
- nodeId: node.id,
1483
- fallbackNodeId: fallbackNode.id,
1484
- fallbackAgentInstanceId: fallbackAgent.instanceId,
1485
- trigger: decision.trigger,
1486
- policy: decision.policy,
1487
- reason,
1488
- startedAt: Date.now(),
1489
- },
1490
- ],
1491
- };
1492
- workflowStore.setRun(run);
1493
-
1494
- await this.executeNode(fallbackNode, workflow, run, outputs, started, completed);
1495
-
1496
- const fallbackRun = run.nodeRuns.find((candidate) => candidate.nodeId === fallbackNode.id);
1497
- if (fallbackRun?.status !== 'completed') {
1498
- return false;
1499
- }
1500
-
1501
- const fallbackOutput = outputs.get(fallbackNode.id) || fallbackRun.outputText;
1502
- if (fallbackOutput) {
1503
- outputs.set(node.id, compactOutputForContext(fallbackOutput));
1504
- }
1505
- completed.add(node.id);
1506
- workflowStore.setRun(run);
1507
- return true;
1508
- }
1509
-
1510
- private maybeAddRepairCycle(
1511
- node: WorkflowNode,
1512
- workflow: Workflow,
1513
- run: WorkflowRun,
1514
- result: TaskResult,
1515
- ): void {
1516
- if (workflow.id !== 'agent_team') return;
1517
- if (!isReviewNode(node) || node.id.startsWith('repair_') || node.id.startsWith('recheck_')) return;
1518
- if (!reviewRequiresRepair(`${result.text}\n${result.error ?? ''}`)) return;
1519
-
1520
- const maxRepairCycles = readMaxRepairCycles(run.metadata);
1521
- if (maxRepairCycles <= 0) return;
1522
-
1523
- const existingCycles = workflow.nodes.filter((candidate) => candidate.id.startsWith(`repair_${node.id}_`)).length;
1524
- if (existingCycles >= maxRepairCycles) return;
1525
-
1526
- if (workflow.nodes.length + 2 > 64) {
1527
- run.metadata = {
1528
- ...run.metadata,
1529
- dynamicRepairSkipped: `Workflow node limit reached after ${node.id}.`,
1530
- };
1531
- workflowStore.setRun(run);
1532
- return;
1533
- }
1534
-
1535
- const fixer = findRepairFixer(workflow, node);
1536
- if (!fixer || fixer.id === node.id) return;
1537
-
1538
- const cycle = existingCycles + 1;
1539
- const repairNode: WorkflowNode = {
1540
- id: `repair_${node.id}_${cycle}`,
1541
- adapterId: fixer.adapterId,
1542
- agentInstanceId: fixer.agentInstanceId,
1543
- agentLabel: fixer.agentLabel ? `${fixer.agentLabel} Repair` : undefined,
1544
- assignment: `Automatic repair from ${node.agentLabel || node.id} review findings`,
1545
- stage: 'repair',
1546
- model: fixer.model,
1547
- permissionMode: fixer.permissionMode,
1548
- toolsSettings: fixer.toolsSettings,
1549
- prompt: [
1550
- 'A review stage found actionable issues in the prior work.',
1551
- 'Use the original user goal, prior implementation outputs, and review output included above.',
1552
- 'Fix only the reported issues; do not restart the whole project or duplicate unrelated work.',
1553
- 'Report changed files, commands, verification, and any remaining blockers.',
1554
- 'Respond in the same language as the user request.',
1555
- ].join('\n'),
1556
- inputs: uniqueInputs([...node.inputs, fixer.id, node.id]),
1557
- output: 'both',
1558
- onFail: 'continue',
1559
- };
1560
- const recheckNode: WorkflowNode = {
1561
- id: `recheck_${node.id}_${cycle}`,
1562
- adapterId: node.adapterId,
1563
- agentInstanceId: node.agentInstanceId,
1564
- agentLabel: node.agentLabel ? `${node.agentLabel} Recheck` : undefined,
1565
- assignment: 'Automatic validation after repair',
1566
- stage: 'review',
1567
- model: node.model,
1568
- permissionMode: node.permissionMode,
1569
- toolsSettings: node.toolsSettings,
1570
- prompt: [
1571
- 'Validate the automatic repair against the original review findings.',
1572
- 'Approve only if the reported issues are fixed.',
1573
- 'If anything remains, list the remaining blockers clearly and do not invent new unrelated scope.',
1574
- 'Respond in the same language as the user request.',
1575
- ].join('\n'),
1576
- inputs: uniqueInputs([node.id, repairNode.id]),
1577
- output: 'message',
1578
- onFail: 'continue',
1579
- };
1580
-
1581
- const finalIndex = workflow.nodes.findIndex((candidate) =>
1582
- candidate.id === 'final_report' || candidate.stage === 'final_report' || candidate.stage === 'report',
1583
- );
1584
- if (finalIndex >= 0) {
1585
- workflow.nodes.splice(finalIndex, 0, repairNode, recheckNode);
1586
- run.nodeRuns.splice(finalIndex, 0, nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
1587
- } else {
1588
- workflow.nodes.push(repairNode, recheckNode);
1589
- run.nodeRuns.push(nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
1590
- }
1591
-
1592
- for (const finalNode of workflow.nodes) {
1593
- if (finalNode.id === 'final_report' || finalNode.stage === 'final_report' || finalNode.stage === 'report') {
1594
- finalNode.inputs = uniqueInputs([...finalNode.inputs, recheckNode.id]);
1595
- }
1596
- }
1597
-
1598
- const repairCycles = Array.isArray(run.metadata?.dynamicRepairCycles)
1599
- ? run.metadata.dynamicRepairCycles
1600
- : [];
1601
- run.metadata = {
1602
- ...run.metadata,
1603
- dynamicRepairCycles: [
1604
- ...repairCycles,
1605
- {
1606
- reviewNodeId: node.id,
1607
- repairNodeId: repairNode.id,
1608
- recheckNodeId: recheckNode.id,
1609
- fixerNodeId: fixer.id,
1610
- },
1611
- ],
1612
- };
1613
- workflowStore.setRun(run);
1614
- }
1615
-
1616
- private async execute(workflow: Workflow, run: WorkflowRun): Promise<void> {
1617
- run.status = 'running';
1618
- workflowStore.setRun(run);
1619
- const completed = new Set<string>();
1620
- const started = new Set<string>();
1621
- const outputs = new Map<string, string>();
1622
- const maxParallelAgents = readMaxParallelAgents(run.metadata);
1623
-
1624
- try {
1625
- while (completed.size < workflow.nodes.length) {
1626
- if (this.isCanceling(run.id)) {
1627
- throw new WorkflowCanceledError();
1628
- }
1629
- const batch = readyNodes(workflow, completed, started);
1630
- if (batch.length === 0) {
1631
- throw new Error('Workflow stalled; no ready nodes remain.');
1632
- }
1633
- for (let index = 0; index < batch.length; index += maxParallelAgents) {
1634
- if (this.isCanceling(run.id)) {
1635
- throw new WorkflowCanceledError();
1636
- }
1637
- const slice = batch.slice(index, index + maxParallelAgents);
1638
- await Promise.all(slice.map((node) => this.executeNode(node, workflow, run, outputs, started, completed)));
1639
- }
1640
- }
1641
- if (this.isCanceling(run.id)) {
1642
- throw new WorkflowCanceledError();
1643
- }
1644
- run.status = 'completed';
1645
- } catch (error) {
1646
- if (error instanceof WorkflowCanceledError || this.isCanceling(run.id)) {
1647
- this.markCanceled(run);
1648
- } else {
1649
- run.status = 'failed';
1650
- run.metadata = {
1651
- ...run.metadata,
1652
- error: error instanceof Error ? error.message : String(error),
1653
- };
1654
- }
1655
- } finally {
1656
- run.finishedAt = run.finishedAt ?? Date.now();
1657
- workflowStore.setRun(run);
1658
- orchestrationTaskService.updateFromWorkflowRun(run);
1659
- notifyWorkflowRunFinished(run);
1660
- const webhookRunStatus = String(run.status);
1661
- dispatchWebhookEvent({
1662
- type: webhookRunStatus === 'completed'
1663
- ? 'run.completed'
1664
- : webhookRunStatus === 'canceled'
1665
- ? 'run.canceled'
1666
- : 'run.failed',
1667
- payload: {
1668
- runId: run.id,
1669
- workflowId: run.workflowId,
1670
- status: webhookRunStatus,
1671
- error: readString(run.metadata?.error),
1672
- },
1673
- });
1674
- this.cancelingRuns.delete(run.id);
1675
- }
1676
- }
1677
-
1678
- private recordPermissionDecision(
1679
- run: WorkflowRun,
1680
- nodeRun: WorkflowNodeRun,
1681
- decision: PermissionDecision,
1682
- ): void {
1683
- nodeRun.permissionDecisions = [
1684
- ...(nodeRun.permissionDecisions ?? []),
1685
- decision,
1686
- ];
1687
-
1688
- const existingApprovals = permissionApprovalRequests(run)
1689
- .filter((approval) => approval.id !== decision.approvalRequest?.id);
1690
- run.metadata = {
1691
- ...run.metadata,
1692
- permissionPolicyEvents: [
1693
- ...permissionPolicyEvents(run),
1694
- decision.event,
1695
- ],
1696
- pendingPermissionApprovals: decision.approvalRequest
1697
- ? [
1698
- ...existingApprovals,
1699
- decision.approvalRequest,
1700
- ]
1701
- : existingApprovals,
1702
- };
1703
-
1704
- if (decision.approvalRequest) {
1705
- notifyPermissionApprovalRequested(run, decision);
1706
- dispatchWebhookEvent({
1707
- type: 'approval.needed',
1708
- payload: {
1709
- runId: run.id,
1710
- workflowId: run.workflowId,
1711
- approvalId: decision.approvalRequest.id,
1712
- capabilities: decision.capabilities,
1713
- },
1714
- });
1715
- }
1716
- }
1717
-
1718
- private async executeNode(
1719
- node: WorkflowNode,
1720
- workflow: Workflow,
1721
- run: WorkflowRun,
1722
- outputs: Map<string, string>,
1723
- started: Set<string>,
1724
- completed: Set<string>,
1725
- ): Promise<void> {
1726
- started.add(node.id);
1727
- const nodeRun = run.nodeRuns.find((candidate) => candidate.nodeId === node.id) as WorkflowNodeRun;
1728
- const enabledAdapters = readEnabledAdapters(run.metadata);
1729
- if (enabledAdapters.length > 0 && !enabledAdapters.includes(node.adapterId)) {
1730
- nodeRun.status = SKIPPED;
1731
- nodeRun.finishedAt = Date.now();
1732
- completed.add(node.id);
1733
- workflowStore.setRun(run);
1734
- return;
1735
- }
1736
- if (this.isCanceling(run.id)) {
1737
- nodeRun.status = 'canceled';
1738
- nodeRun.finishedAt = Date.now();
1739
- workflowStore.setRun(run);
1740
- throw new WorkflowCanceledError();
1741
- }
1742
-
1743
- nodeRun.status = 'running';
1744
- nodeRun.startedAt = Date.now();
1745
- nodeRun.permissionMode = resolveNodePermissionMode(node, resolveWorkflowWorkspace(run.metadata));
1746
- workflowStore.setRun(run);
1747
-
1748
- const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
1749
- const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
1750
- const contextPacket = buildWorkflowContextPacket({
1751
- run,
1752
- node,
1753
- workspaceTarget,
1754
- inputContext,
1755
- inputNodeIds: node.inputs,
1756
- });
1757
- nodeRun.contextPacket = contextPacket;
1758
- workflowStore.setRun(run);
1759
- const prompt = [
1760
- 'Original user request (primary task; answer this directly even if the workspace is empty):',
1761
- run.input?.trim() || '(No original user request was provided.)',
1762
- formatContextPacketForPrompt(contextPacket),
1763
- inputContext
1764
- ? `Upstream workflow context from prior agents:\n${inputContext}`
1765
- : '',
1766
- `Current workflow step instructions:\n${node.prompt}`,
1767
- workspaceContextPrompt(workspaceTarget),
1768
- ].filter(Boolean).join('\n\n');
1769
- const settings = getMetadataRecord(run.metadata, 'settings');
1770
- const projectPath = workspaceTarget.projectPath;
1771
- const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
1772
- const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
1773
- const baseRef = readString(settings.baseRef) ?? 'HEAD';
1774
- const effectivePermissionMode = resolveNodePermissionMode(node, workspaceTarget);
1775
- const effectiveModel = await resolveWorkflowModel(node.adapterId, node.model);
1776
- if (effectiveModel !== node.model) {
1777
- nodeRun.model = effectiveModel;
1778
- const modelFallbackEvents = Array.isArray(run.metadata?.modelFallbackEvents)
1779
- ? run.metadata.modelFallbackEvents
1780
- : [];
1781
- run.metadata = {
1782
- ...run.metadata,
1783
- modelFallbackEvents: [
1784
- ...modelFallbackEvents,
1785
- {
1786
- nodeId: node.id,
1787
- adapterId: node.adapterId,
1788
- requestedModel: node.model,
1789
- effectiveModel,
1790
- changedAt: Date.now(),
1791
- },
1792
- ],
1793
- };
1794
- workflowStore.setRun(run);
1795
- }
1796
- const permissionPolicy = permissionPolicyFromRun(run);
1797
- nodeRun.permissionPolicy = permissionPolicy;
1798
- const permissionDecision = evaluatePermissionRequest({
1799
- policy: permissionPolicy,
1800
- request: {
1801
- source: 'workflow_node',
1802
- toolName: node.adapterId,
1803
- input: {
1804
- assignment: node.assignment,
1805
- stage: node.stage,
1806
- toolsSettings: node.toolsSettings,
1807
- },
1808
- cwd: projectPath,
1809
- workspacePath: workspaceTarget.appRoot,
1810
- targetPaths: [projectPath],
1811
- summary: [
1812
- node.agentLabel || node.id,
1813
- node.stage ? `stage=${node.stage}` : undefined,
1814
- node.assignment,
1815
- ].filter(Boolean).join(' / '),
1816
- },
1817
- context: {
1818
- runId: run.id,
1819
- nodeId: node.id,
1820
- workflowId: run.workflowId,
1821
- adapterId: node.adapterId,
1822
- agentLabel: node.agentLabel,
1823
- userId: readNotificationUserId(run.metadata),
1824
- },
1825
- });
1826
- this.recordPermissionDecision(run, nodeRun, permissionDecision);
1827
- workflowStore.setRun(run);
1828
- if (permissionDecision.behavior === 'deny') {
1829
- nodeRun.finishedAt = Date.now();
1830
- nodeRun.status = 'failed';
1831
- nodeRun.error = permissionDecision.message;
1832
- workflowStore.setRun(run);
1833
- if (node.onFail === 'continue') {
1834
- completed.add(node.id);
1835
- return;
1836
- }
1837
- throw new Error(permissionDecision.message);
1838
- }
1839
- let body: { id?: string; error?: { message?: string } };
1840
- try {
1841
- const submit = await fetch(`${localHermesBaseUrl()}/tasks`, {
1842
- method: 'POST',
1843
- headers: { 'content-type': 'application/json' },
1844
- body: JSON.stringify({
1845
- adapterId: node.adapterId,
1846
- contextId: run.contextId,
1847
- message: {
1848
- messageId: newId('msg'),
1849
- role: 'user',
1850
- parts: [{ kind: 'text', text: prompt }],
1851
- },
1852
- metadata: {
1853
- workflowRunId: run.id,
1854
- workflowNodeId: node.id,
1855
- agentInstanceId: node.agentInstanceId,
1856
- agentLabel: node.agentLabel,
1857
- assignment: node.assignment,
1858
- model: effectiveModel,
1859
- permissionMode: effectivePermissionMode,
1860
- permissionPolicy,
1861
- permissionPolicyContext: {
1862
- runId: run.id,
1863
- nodeId: node.id,
1864
- workflowId: run.workflowId,
1865
- adapterId: node.adapterId,
1866
- agentLabel: node.agentLabel,
1867
- userId: readNotificationUserId(run.metadata),
1868
- },
1869
- toolsSettings: node.toolsSettings,
1870
- projectPath,
1871
- workspaceTarget: workspaceTargetMetadata(workspaceTarget),
1872
- workspace: {
1873
- kind: isolation,
1874
- projectPath,
1875
- baseRef,
1876
- keepAfterCompletion,
1877
- },
1878
- },
1879
- }),
1880
- });
1881
- body = await submit.json() as { id?: string; error?: { message?: string } };
1882
- if (!submit.ok || !body.id) {
1883
- throw new Error(body.error?.message ?? `Workflow node ${node.id} submit failed.`);
1884
- }
1885
- } catch (error) {
1886
- nodeRun.finishedAt = Date.now();
1887
- nodeRun.status = 'failed';
1888
- nodeRun.error = error instanceof Error ? error.message : String(error);
1889
- workflowStore.setRun(run);
1890
- if (isExternalDirectoryPermissionError(nodeRun.error)) {
1891
- completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1892
- workflowStore.setRun(run);
1893
- return;
1894
- }
1895
- if (await this.runFallbackAfterFailure(
1896
- node,
1897
- workflow,
1898
- run,
1899
- outputs,
1900
- started,
1901
- completed,
1902
- nodeRun.error,
1903
- 'provider_failure',
1904
- )) {
1905
- return;
1906
- }
1907
- if (node.onFail === 'continue') {
1908
- completed.add(node.id);
1909
- return;
1910
- }
1911
- throw error;
1912
- }
1913
- nodeRun.hermesTaskId = body.id;
1914
- workflowStore.setRun(run);
1915
-
1916
- if (this.isCanceling(run.id)) {
1917
- await cancelHermesTask(body.id);
1918
- nodeRun.status = 'canceled';
1919
- nodeRun.finishedAt = Date.now();
1920
- workflowStore.setRun(run);
1921
- throw new WorkflowCanceledError();
1922
- }
1923
-
1924
- let result: TaskResult;
1925
- try {
1926
- result = await waitForTask(
1927
- body.id,
1928
- () => this.isCanceling(run.id),
1929
- (snapshot) => {
1930
- nodeRun.outputText = snapshot.text || nodeRun.outputText;
1931
- nodeRun.messages = snapshot.messages;
1932
- nodeRun.artifacts = snapshot.artifacts;
1933
- nodeRun.error = snapshot.error;
1934
- workflowStore.setRun(run);
1935
- },
1936
- node.timeoutMs,
1937
- );
1938
- } catch (error) {
1939
- if (!(error instanceof WorkflowNodeTimeoutError)) {
1940
- throw error;
1941
- }
1942
-
1943
- await cancelHermesTask(body.id);
1944
- nodeRun.finishedAt = Date.now();
1945
- nodeRun.status = 'failed';
1946
- nodeRun.error = error.message;
1947
- if (nodeRun.outputText) {
1948
- outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1949
- }
1950
- workflowStore.setRun(run);
1951
- if (isExternalDirectoryPermissionError(nodeRun.error)) {
1952
- completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1953
- workflowStore.setRun(run);
1954
- return;
1955
- }
1956
- if (await this.runFallbackAfterFailure(
1957
- node,
1958
- workflow,
1959
- run,
1960
- outputs,
1961
- started,
1962
- completed,
1963
- nodeRun.error,
1964
- 'timeout',
1965
- )) {
1966
- return;
1967
- }
1968
- if (node.onFail === 'continue') {
1969
- completed.add(node.id);
1970
- return;
1971
- }
1972
- throw error;
1973
- }
1974
- nodeRun.finishedAt = Date.now();
1975
- nodeRun.outputText = result.text;
1976
- nodeRun.messages = result.messages;
1977
- nodeRun.artifacts = result.artifacts;
1978
- if (this.isCanceling(run.id)) {
1979
- nodeRun.status = 'canceled';
1980
- workflowStore.setRun(run);
1981
- throw new WorkflowCanceledError();
1982
- }
1983
- if (result.state === 'completed') {
1984
- let outputForContext = result.text;
1985
- if (requiresHandoffArtifact(node)) {
1986
- const handoffParse = parseHandoffArtifact(handoffArtifactSource(result), {
1987
- workflowRunId: run.id,
1988
- nodeId: node.id,
1989
- agentLabel: node.agentLabel,
1990
- stage: node.stage,
1991
- });
1992
- if (!handoffParse.ok) {
1993
- const visibleHandoffError = handoffParse.error.startsWith('Invalid handoff artifact')
1994
- ? handoffParse.error
1995
- : `Invalid handoff artifact: ${handoffParse.error}`;
1996
- nodeRun.status = 'failed';
1997
- nodeRun.error = visibleHandoffError;
1998
- workflowStore.setRun(run);
1999
- if (await this.runFallbackAfterFailure(
2000
- node,
2001
- workflow,
2002
- run,
2003
- outputs,
2004
- started,
2005
- completed,
2006
- visibleHandoffError,
2007
- 'invalid_output',
2008
- )) {
2009
- return;
2010
- }
2011
- if (node.onFail === 'continue') {
2012
- completed.add(node.id);
2013
- return;
2014
- }
2015
- throw new Error(visibleHandoffError);
2016
- }
2017
-
2018
- nodeRun.handoffArtifact = handoffParse.artifact;
2019
- nodeRun.artifacts = [
2020
- ...(nodeRun.artifacts ?? []).filter((artifact) => artifact.type !== 'handoff-artifact'),
2021
- handoffArtifactToWorkflowArtifact(handoffParse.artifact),
2022
- ];
2023
- outputForContext = formatHandoffArtifactForContext(handoffParse.artifact);
2024
- }
2025
-
2026
- outputs.set(node.id, compactOutputForContext(outputForContext));
2027
- completed.add(node.id);
2028
- nodeRun.status = 'completed';
2029
- workflowStore.setRun(run);
2030
- this.maybeAddRepairCycle(node, workflow, run, result);
2031
- return;
2032
- }
2033
- if (result.state === 'canceled') {
2034
- nodeRun.status = 'canceled';
2035
- workflowStore.setRun(run);
2036
- throw new WorkflowCanceledError();
2037
- }
2038
-
2039
- nodeRun.status = 'failed';
2040
- nodeRun.error = result.error ?? `Hermes task ended with ${result.state}`;
2041
- workflowStore.setRun(run);
2042
- if (isExternalDirectoryPermissionError(`${nodeRun.error}\n${nodeRun.outputText ?? ''}`)) {
2043
- completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
2044
- workflowStore.setRun(run);
2045
- return;
2046
- }
2047
- if (await this.runFallbackAfterFailure(
2048
- node,
2049
- workflow,
2050
- run,
2051
- outputs,
2052
- started,
2053
- completed,
2054
- nodeRun.error,
2055
- classifyWorkflowFailure(`${nodeRun.error}\n${nodeRun.outputText ?? ''}`),
2056
- )) {
2057
- return;
2058
- }
2059
- if (node.onFail === 'continue') {
2060
- if (nodeRun.outputText) {
2061
- outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
2062
- }
2063
- completed.add(node.id);
2064
- return;
2065
- }
2066
- throw new Error(nodeRun.error);
2067
- }
2068
- }
2069
-
2070
- export const workflowRunner = new WorkflowRunner();
1
+ import crypto from 'node:crypto';
2
+
3
+ import type {
4
+ Workflow,
5
+ WorkflowNode,
6
+ WorkflowNodeRun,
7
+ WorkflowRun,
8
+ } from '@/modules/orchestration/workflows/workflow.types.js';
9
+ import {
10
+ PIXCODE_HANDOFF_PROTOCOL,
11
+ formatHandoffArtifactForContext,
12
+ handoffArtifactToWorkflowArtifact,
13
+ parseHandoffArtifact,
14
+ } from '@/modules/orchestration/workflows/handoff-artifact.js';
15
+ import {
16
+ buildWorkflowContextPacket,
17
+ formatContextPacketForPrompt,
18
+ } from '@/modules/orchestration/workflows/context-packet.js';
19
+ import {
20
+ type WorkflowFallbackTrigger,
21
+ classifyWorkflowFailure,
22
+ resolveWorkflowFallbackDecision,
23
+ } from '@/modules/orchestration/workflows/workflow-fallback-policy.js';
24
+ import {
25
+ evaluatePermissionRequest,
26
+ resolvePermissionPolicyFromMetadata,
27
+ type PermissionDecision,
28
+ type PermissionPolicy,
29
+ type PermissionPolicyEvent,
30
+ } from '@/modules/orchestration/security/permission-policy.js';
31
+ import {
32
+ type ResolvedWorkspaceTarget,
33
+ resolveWorkflowWorkspace,
34
+ workspaceContextPrompt,
35
+ workspaceTargetMetadata,
36
+ } from '@/modules/orchestration/workflows/workspace-target.js';
37
+ import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
38
+ import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
39
+ // @ts-ignore — plain-JS service
40
+ import {
41
+ getDefaultProviderModel,
42
+ getProviderModelRegistryEntry,
43
+ getStaticProviderModels,
44
+ } from '@/services/model-registry.js';
45
+ // @ts-ignore — plain-JS service
46
+ import {
47
+ createNotificationEvent,
48
+ notifyRunFailed,
49
+ notifyRunStopped,
50
+ notifyUserIfEnabled,
51
+ } from '@/services/notification-orchestrator.js';
52
+ // @ts-ignore — plain-JS service
53
+ import { dispatchWebhookEvent } from '@/services/webhooks.js';
54
+
55
+ const TERMINAL = new Set(['completed', 'failed', 'canceled']);
56
+ const SKIPPED = 'skipped';
57
+ const BACKEND_HANDOFF_TIMEOUT_MS = 120_000;
58
+ const MAX_OUTPUT_CONTEXT_CHARS = 12_000;
59
+ const DEFAULT_MAX_REPAIR_CYCLES = 1;
60
+ const MAX_REPAIR_CYCLES = 5;
61
+ const HANDOFF_ARTIFACT_EXAMPLE = [
62
+ '{',
63
+ ' "protocol": "pixcode.handoff.v1",',
64
+ ' "taskStatus": "ready | completed | blocked | failed | needs-review",',
65
+ ' "contextSummary": "Compacted context the next agent needs.",',
66
+ ' "taskResult": "What was decided or completed in this step.",',
67
+ ' "changedFiles": [],',
68
+ ' "blockers": [],',
69
+ ' "risks": [],',
70
+ ' "nextAction": "The requested next action.",',
71
+ ' "nextInstructions": "Specific instructions for the next agent."',
72
+ '}',
73
+ ].join('\n');
74
+ const KNOWN_AGENT_ROLES = [
75
+ 'backend',
76
+ 'frontend',
77
+ 'review',
78
+ 'implementation',
79
+ 'proposal',
80
+ 'critique',
81
+ 'response',
82
+ 'decision',
83
+ 'report',
84
+ ] as const;
85
+
86
+ class WorkflowCanceledError extends Error {
87
+ constructor() {
88
+ super('Workflow canceled.');
89
+ this.name = 'WorkflowCanceledError';
90
+ }
91
+ }
92
+
93
+ class WorkflowNodeTimeoutError extends Error {
94
+ constructor(readonly timeoutMs: number) {
95
+ super(`Workflow node timed out after ${Math.round(timeoutMs / 1000)}s.`);
96
+ this.name = 'WorkflowNodeTimeoutError';
97
+ }
98
+ }
99
+
100
+ function newId(prefix: string): string {
101
+ return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
102
+ }
103
+
104
+ function localHermesBaseUrl(): string {
105
+ return `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}/hermes`;
106
+ }
107
+
108
+ function validateWorkflow(workflow: Workflow): void {
109
+ if (workflow.nodes.length > 64) {
110
+ throw new Error('Workflow node limit exceeded.');
111
+ }
112
+ const ids = new Set(workflow.nodes.map((node) => node.id));
113
+ for (const node of workflow.nodes) {
114
+ for (const input of node.inputs) {
115
+ if (!ids.has(input)) {
116
+ throw new Error(`Workflow node ${node.id} references missing input ${input}.`);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ type TaskResult = {
123
+ state: string;
124
+ text: string;
125
+ error?: string;
126
+ messages: Array<{ role: string; text: string }>;
127
+ artifacts: Array<{
128
+ type: string;
129
+ text?: string;
130
+ data?: Record<string, unknown>;
131
+ metadata?: Record<string, unknown>;
132
+ }>;
133
+ };
134
+
135
+ type RawTask = {
136
+ state?: string;
137
+ error?: { code?: string; message?: string };
138
+ history?: Array<{ role?: string; parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }> }>;
139
+ artifacts?: Array<{
140
+ type?: string;
141
+ parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }>;
142
+ metadata?: Record<string, unknown>;
143
+ }>;
144
+ };
145
+
146
+ type AgentAssignment = {
147
+ instanceId: string;
148
+ adapterId: string;
149
+ label: string;
150
+ role?: AgentRole;
151
+ instruction?: string;
152
+ model?: string;
153
+ permissionMode?: string;
154
+ toolsSettings?: Record<string, unknown>;
155
+ order: number;
156
+ };
157
+
158
+ type KnownAgentRole = typeof KNOWN_AGENT_ROLES[number];
159
+ type AgentRole = string;
160
+ type ProviderId = 'claude' | 'cursor' | 'codex' | 'gemini' | 'qwen' | 'opencode';
161
+ type ProviderModel = {
162
+ value: string;
163
+ label?: string;
164
+ source?: 'static' | 'api';
165
+ free?: boolean;
166
+ };
167
+ type RunStoppedNotifier = (payload: {
168
+ userId: string | number;
169
+ provider: string;
170
+ sessionId?: string | null;
171
+ stopReason?: string;
172
+ sessionName?: string | null;
173
+ }) => void;
174
+ type RunFailedNotifier = (payload: {
175
+ userId: string | number;
176
+ provider: string;
177
+ sessionId?: string | null;
178
+ error: unknown;
179
+ sessionName?: string | null;
180
+ }) => void;
181
+
182
+ const sendRunStoppedNotification = notifyRunStopped as RunStoppedNotifier;
183
+ const sendRunFailedNotification = notifyRunFailed as RunFailedNotifier;
184
+
185
+ const adapterProviderMap: Record<string, ProviderId | undefined> = {
186
+ 'claude-code': 'claude',
187
+ cursor: 'cursor',
188
+ codex: 'codex',
189
+ gemini: 'gemini',
190
+ qwen: 'qwen',
191
+ opencode: 'opencode',
192
+ };
193
+
194
+ function readAgentRole(value: unknown): AgentRole | undefined {
195
+ return typeof value === 'string' && value.trim() && value.trim() !== 'auto'
196
+ ? value.trim()
197
+ : undefined;
198
+ }
199
+
200
+ function isKnownAgentRole(value: string | undefined): value is KnownAgentRole {
201
+ return Boolean(value && (KNOWN_AGENT_ROLES as readonly string[]).includes(value));
202
+ }
203
+
204
+ function getMetadataRecord(metadata: Record<string, unknown> | undefined, key: string): Record<string, unknown> {
205
+ return readRecord(metadata?.[key]) ?? {};
206
+ }
207
+
208
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
209
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
210
+ }
211
+
212
+ function readString(value: unknown): string | undefined {
213
+ return typeof value === 'string' && value.trim() ? value : undefined;
214
+ }
215
+
216
+ function readNotificationUserId(metadata?: Record<string, unknown>): string | number | null {
217
+ const value = metadata?.userId;
218
+ return typeof value === 'string' || typeof value === 'number' ? value : null;
219
+ }
220
+
221
+ function workflowNotificationTitle(run: WorkflowRun): string {
222
+ return readString(run.metadata?.workflowName) ?? run.workflowId;
223
+ }
224
+
225
+ function notifyWorkflowRunFinished(run: WorkflowRun): void {
226
+ const userId = readNotificationUserId(run.metadata);
227
+ if (!userId) return;
228
+
229
+ if (run.status === 'completed') {
230
+ sendRunStoppedNotification({
231
+ userId,
232
+ provider: 'system',
233
+ sessionId: run.id,
234
+ sessionName: workflowNotificationTitle(run),
235
+ stopReason: 'Orchestration completed',
236
+ });
237
+ return;
238
+ }
239
+
240
+ if (run.status === 'canceled') {
241
+ sendRunStoppedNotification({
242
+ userId,
243
+ provider: 'system',
244
+ sessionId: run.id,
245
+ sessionName: workflowNotificationTitle(run),
246
+ stopReason: 'Orchestration canceled',
247
+ });
248
+ return;
249
+ }
250
+
251
+ if (run.status === 'failed') {
252
+ sendRunFailedNotification({
253
+ userId,
254
+ provider: 'system',
255
+ sessionId: run.id,
256
+ sessionName: workflowNotificationTitle(run),
257
+ error: readString(run.metadata?.error) ?? 'Orchestration failed',
258
+ });
259
+ }
260
+ }
261
+
262
+ function permissionPolicyFromRun(run: WorkflowRun): PermissionPolicy {
263
+ return resolvePermissionPolicyFromMetadata(run.metadata);
264
+ }
265
+
266
+ function permissionPolicyEvents(run: WorkflowRun): PermissionPolicyEvent[] {
267
+ return Array.isArray(run.metadata?.permissionPolicyEvents)
268
+ ? run.metadata.permissionPolicyEvents.filter((event): event is PermissionPolicyEvent =>
269
+ Boolean(event && typeof event === 'object'),
270
+ )
271
+ : [];
272
+ }
273
+
274
+ function permissionApprovalRequests(run: WorkflowRun): Array<Record<string, unknown>> {
275
+ return Array.isArray(run.metadata?.pendingPermissionApprovals)
276
+ ? run.metadata.pendingPermissionApprovals.filter((event): event is Record<string, unknown> =>
277
+ Boolean(event && typeof event === 'object'),
278
+ )
279
+ : [];
280
+ }
281
+
282
+ function notifyPermissionApprovalRequested(run: WorkflowRun, decision: PermissionDecision): void {
283
+ const userId = readNotificationUserId(run.metadata);
284
+ if (!userId || !decision.approvalRequest) return;
285
+
286
+ const event = (createNotificationEvent as unknown as (payload: Record<string, unknown>) => unknown)({
287
+ provider: 'system',
288
+ sessionId: run.id,
289
+ kind: 'action_required',
290
+ code: 'permission.required',
291
+ meta: {
292
+ toolName: decision.capabilities.join(', '),
293
+ sessionName: workflowNotificationTitle(run),
294
+ },
295
+ severity: 'warning',
296
+ requiresUserAction: true,
297
+ dedupeKey: `workflow:permission:${run.id}:${decision.requestId}`,
298
+ });
299
+ (notifyUserIfEnabled as (payload: { userId: string | number; event: unknown }) => void)({
300
+ userId,
301
+ event,
302
+ });
303
+ }
304
+
305
+ function readBoolean(value: unknown): boolean | undefined {
306
+ return typeof value === 'boolean' ? value : undefined;
307
+ }
308
+
309
+ function modelValueSet(models: ProviderModel[]): Set<string> {
310
+ return new Set(models.map((model) => model.value).filter(Boolean));
311
+ }
312
+
313
+ function preferredFallbackModel(models: ProviderModel[], defaultModel?: string): string | undefined {
314
+ const values = modelValueSet(models);
315
+ if (defaultModel && values.has(defaultModel)) return defaultModel;
316
+ return models.find((model) => model.source === 'api' && model.free)?.value
317
+ ?? models.find((model) => model.source === 'api')?.value
318
+ ?? models.find((model) => model.free)?.value
319
+ ?? models[0]?.value
320
+ ?? defaultModel;
321
+ }
322
+
323
+ async function resolveWorkflowModel(adapterId: string, requestedModel?: string): Promise<string | undefined> {
324
+ const provider = adapterProviderMap[adapterId];
325
+ if (!provider) return requestedModel;
326
+
327
+ const defaultModel = getDefaultProviderModel(provider);
328
+ if (!requestedModel) return defaultModel;
329
+
330
+ try {
331
+ const result = await getProviderModelRegistryEntry(provider);
332
+ const models = Array.isArray(result?.models) ? result.models as ProviderModel[] : [];
333
+ if (modelValueSet(models).has(requestedModel)) {
334
+ return requestedModel;
335
+ }
336
+ return preferredFallbackModel(models, defaultModel) ?? requestedModel;
337
+ } catch {
338
+ const staticModels = getStaticProviderModels(provider) as ProviderModel[];
339
+ const staticValues = modelValueSet(staticModels);
340
+ return staticValues.has(requestedModel)
341
+ ? requestedModel
342
+ : preferredFallbackModel(staticModels, defaultModel) ?? requestedModel;
343
+ }
344
+ }
345
+
346
+ function readIsolation(value: unknown): 'host' | 'worktree' | 'docker' | undefined {
347
+ return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
348
+ }
349
+
350
+ function readLegacyEnabledAdapters(metadata?: Record<string, unknown>): string[] {
351
+ return Array.isArray(metadata?.enabledAdapters)
352
+ ? metadata.enabledAdapters.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
353
+ : [];
354
+ }
355
+
356
+ function readMetadataAgents(metadata?: Record<string, unknown>): AgentAssignment[] {
357
+ if (!Array.isArray(metadata?.agents)) return [];
358
+
359
+ return metadata.agents
360
+ .map((value, index): AgentAssignment | null => {
361
+ if (!value || typeof value !== 'object') return null;
362
+ const record = value as Record<string, unknown>;
363
+ const adapterId = readString(record.adapterId);
364
+ if (!adapterId) return null;
365
+ if (readBoolean(record.enabled) === false) return null;
366
+
367
+ return {
368
+ instanceId: readString(record.instanceId) ?? `${adapterId}-${index + 1}`,
369
+ adapterId,
370
+ label: readString(record.label) ?? `${adapterId} #${index + 1}`,
371
+ role: readAgentRole(record.role),
372
+ instruction: readString(record.instruction),
373
+ model: readString(record.model),
374
+ permissionMode: readString(record.permissionMode),
375
+ toolsSettings: readRecord(record.toolsSettings),
376
+ order: index,
377
+ };
378
+ })
379
+ .filter((value): value is AgentAssignment => Boolean(value))
380
+ .slice(0, 16);
381
+ }
382
+
383
+ function readAgentAssignments(metadata?: Record<string, unknown>): AgentAssignment[] {
384
+ const agents = readMetadataAgents(metadata);
385
+ if (agents.length > 0) return agents;
386
+
387
+ return readLegacyEnabledAdapters(metadata).map((adapterId, index) => ({
388
+ instanceId: `${adapterId}-${index + 1}`,
389
+ adapterId,
390
+ label: `${adapterId} #${index + 1}`,
391
+ order: index,
392
+ }));
393
+ }
394
+
395
+ function readEnabledAdapters(metadata?: Record<string, unknown>): string[] {
396
+ return [...new Set(readAgentAssignments(metadata).map((agent) => agent.adapterId))];
397
+ }
398
+
399
+ function readMaxParallelAgents(metadata?: Record<string, unknown>): number {
400
+ const settings = getMetadataRecord(metadata, 'settings');
401
+ const value = settings.maxParallelAgents;
402
+ return typeof value === 'number' && Number.isFinite(value)
403
+ ? Math.max(1, Math.min(12, Math.round(value)))
404
+ : 3;
405
+ }
406
+
407
+ function readMaxRepairCycles(metadata?: Record<string, unknown>): number {
408
+ const settings = getMetadataRecord(metadata, 'settings');
409
+ const value = settings.maxRepairCycles;
410
+ return typeof value === 'number' && Number.isFinite(value)
411
+ ? Math.max(0, Math.min(MAX_REPAIR_CYCLES, Math.round(value)))
412
+ : DEFAULT_MAX_REPAIR_CYCLES;
413
+ }
414
+
415
+ function safeNodeId(adapterId: string, suffix: string): string {
416
+ return `${adapterId.replace(/[^a-zA-Z0-9_]+/g, '_')}_${suffix}`;
417
+ }
418
+
419
+ function safeAgentNodeId(agent: AgentAssignment, index: number, suffix: string): string {
420
+ return `agent_${index + 1}_${safeNodeId(agent.adapterId, suffix)}`;
421
+ }
422
+
423
+ function agentRoster(agents: AgentAssignment[]): string {
424
+ return agents
425
+ .map((agent, index) => {
426
+ const instruction = agent.instruction
427
+ ? `\n User assignment: ${agent.instruction}`
428
+ : '';
429
+ const role = agent.role ? `\n API role: ${agent.role}` : '';
430
+ return `${index + 1}. ${agent.label} (${agent.adapterId})${role}${instruction}`;
431
+ })
432
+ .join('\n');
433
+ }
434
+
435
+ function inferAgentRole(agent: AgentAssignment): AgentRole {
436
+ if (isKnownAgentRole(agent.role)) return agent.role;
437
+
438
+ const text = `${agent.label} ${agent.adapterId} ${agent.role ?? ''} ${agent.instruction ?? ''}`.toLocaleLowerCase('tr');
439
+ if (/(test|tester|qa|review|code review|hata|kontrol|onay|incele|doğrula|dogrula)/u.test(text)) {
440
+ return 'review';
441
+ }
442
+ if (/(backend|back-end|api|server|veri|database|db|fapi|endpoint|websocket|ws)/u.test(text)) {
443
+ return 'backend';
444
+ }
445
+ if (/(frontend|front-end|ui|ux|tailwind|tasarım|tasarim|design|chart|tradingview|arayüz|arayuz)/u.test(text)) {
446
+ return 'frontend';
447
+ }
448
+ return 'implementation';
449
+ }
450
+
451
+ function inferImplementationRole(agent: AgentAssignment): 'backend' | 'frontend' | 'review' | 'implementation' {
452
+ const role = inferAgentRole(agent);
453
+ return role === 'backend' || role === 'frontend' || role === 'review' || role === 'implementation'
454
+ ? role
455
+ : 'implementation';
456
+ }
457
+
458
+ function displayStage(agent: AgentAssignment, fallback: AgentRole): string {
459
+ return agent.role && !isKnownAgentRole(agent.role) ? agent.role : fallback;
460
+ }
461
+
462
+ function rolePrompt(role: AgentRole): string {
463
+ if (role === 'backend') {
464
+ return 'Backend/API/data work should define stable contracts first. Report endpoints, payload shapes, ports, and any data-source limitations clearly for downstream agents.';
465
+ }
466
+ if (role === 'frontend') {
467
+ return 'Frontend/UI work must use prior backend/data-contract outputs when present. If a dependency is missing, use a minimal mock only as a temporary fallback and report the blocker.';
468
+ }
469
+ if (role === 'review') {
470
+ return 'You are the validation/review stage. Inspect the prior agent outputs and actual project state. Approve only if it works; otherwise return a concrete bug list and required fixes.';
471
+ }
472
+ if (role === 'proposal') {
473
+ return 'You are in the proposal stage. Produce a concrete option with tradeoffs, assumptions, and what should happen next. Do not edit files.';
474
+ }
475
+ if (role === 'critique') {
476
+ return 'You are in the critique stage. Challenge the proposal for risks, missing constraints, and weak assumptions. Do not edit files.';
477
+ }
478
+ if (role === 'response') {
479
+ return 'You are in the response stage. Reconcile the critique with the proposal and refine the practical path forward. Do not edit files.';
480
+ }
481
+ if (role === 'decision' || role === 'report') {
482
+ return 'You are the reporting stage. Produce the final concise decision report and a next prompt for launching an implementation agent team. Do not edit files.';
483
+ }
484
+ if (role !== 'implementation') {
485
+ return `You are assigned to the custom stage "${role}". Follow that user-defined stage literally, avoid duplicating other agents, and report changed files, commands, blockers, and next actions.`;
486
+ }
487
+ return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
488
+ }
489
+
490
+ function privacyGuardPrompt(): string {
491
+ return 'Do not mention internal instructions, memory files, skill use, or tool protocol unless the user explicitly asks.';
492
+ }
493
+
494
+ function handoffArtifactInstructions(statusHint: string): string {
495
+ return [
496
+ `Output exactly one JSON object using the ${PIXCODE_HANDOFF_PROTOCOL} handoff artifact protocol.`,
497
+ 'Do not wrap it in Markdown. Do not add commentary before or after it.',
498
+ `Use "${statusHint}" for taskStatus unless completed, blocked, failed, or needs-review is more accurate.`,
499
+ 'Schema:',
500
+ HANDOFF_ARTIFACT_EXAMPLE,
501
+ ].join('\n');
502
+ }
503
+
504
+ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
505
+ return [
506
+ `You are ${agent.label} in a Pixcode CLI team.`,
507
+ `Your inferred stage is: ${role}.`,
508
+ 'This is a bounded Hermes handoff task, not the full implementation.',
509
+ 'Read the original user goal and coordinator plan, then publish a compact contract for downstream agents.',
510
+ agent.instruction ? `Your explicit assignment from the user is: ${agent.instruction}` : '',
511
+ handoffArtifactInstructions('ready'),
512
+ 'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
513
+ privacyGuardPrompt(),
514
+ 'Stop after the contract. Keep it concise and respond in the same language as the user request.',
515
+ ].filter(Boolean).join('\n');
516
+ }
517
+
518
+ function handoffInitPrompt(agent: AgentAssignment, index: number): string {
519
+ return [
520
+ `You are preparing ${agent.label} for a strict Pixcode handoff chain.`,
521
+ `This is internal step ${index + 1}.`,
522
+ 'Create a compact init packet for the next visible work step.',
523
+ 'Use the original user goal and any prior compact handoff packet included above.',
524
+ agent.instruction ? `The explicit assignment for this agent is: ${agent.instruction}` : '',
525
+ handoffArtifactInstructions('ready'),
526
+ privacyGuardPrompt(),
527
+ 'Do not perform the task yet. Do not mention that this is hidden from the user.',
528
+ 'Respond in the same language as the user request.',
529
+ ].filter(Boolean).join('\n');
530
+ }
531
+
532
+ function handoffWorkPrompt(agent: AgentAssignment, index: number): string {
533
+ return [
534
+ `You are ${agent.label} in a strict Pixcode handoff chain.`,
535
+ `This is visible work step ${index + 1}.`,
536
+ 'The internal init packet above is your starting context. Do the assigned work now.',
537
+ agent.instruction
538
+ ? `Your explicit assignment from the user is: ${agent.instruction}`
539
+ : 'Use the init packet and original user goal to choose the next useful work for this step.',
540
+ rolePrompt(agent.role ?? 'implementation'),
541
+ privacyGuardPrompt(),
542
+ 'Report only user-facing progress, changed files, commands, verification, blockers, and next actions.',
543
+ 'Respond in the same language as the user request.',
544
+ ].filter(Boolean).join('\n');
545
+ }
546
+
547
+ function handoffCompactPrompt(agent: AgentAssignment, index: number): string {
548
+ return [
549
+ `You are compacting ${agent.label}'s strict handoff output for the next Pixcode agent.`,
550
+ `This is internal compact step ${index + 1}.`,
551
+ 'Read the prior visible work output included above and create a compact handoff packet.',
552
+ handoffArtifactInstructions('completed'),
553
+ privacyGuardPrompt(),
554
+ 'Do not include raw logs unless they are essential. Keep it concise and actionable.',
555
+ 'Respond in the same language as the user request.',
556
+ ].join('\n');
557
+ }
558
+
559
+ function compactOutputForContext(text: string): string {
560
+ if (text.length <= MAX_OUTPUT_CONTEXT_CHARS) {
561
+ return text;
562
+ }
563
+
564
+ const edge = Math.floor(MAX_OUTPUT_CONTEXT_CHARS / 2);
565
+ return [
566
+ text.slice(0, edge),
567
+ `\n\n[...${text.length - MAX_OUTPUT_CONTEXT_CHARS} characters omitted from prior agent output...]\n\n`,
568
+ text.slice(-edge),
569
+ ].join('');
570
+ }
571
+
572
+ function requiresHandoffArtifact(node: WorkflowNode): boolean {
573
+ return node.stage === 'handoff' || node.stage === 'handoff_init' || node.stage === 'handoff_compact';
574
+ }
575
+
576
+ function handoffArtifactSource(result: TaskResult): string {
577
+ const structured = result.artifacts.find((artifact) => artifact.type === 'handoff-artifact' && artifact.data);
578
+ if (structured?.data) {
579
+ return JSON.stringify(structured.data);
580
+ }
581
+ return result.text;
582
+ }
583
+
584
+ function isExternalDirectoryPermissionError(value: unknown): boolean {
585
+ const text = String(value ?? '').toLocaleLowerCase('en');
586
+ return (
587
+ text.includes('external_directory') ||
588
+ /permission requested:.*auto-rejecting/u.test(text) ||
589
+ /auto-rejecting.*permission/u.test(text) ||
590
+ /outside (the )?(workspace|working directory)/u.test(text) ||
591
+ /permission.*external/u.test(text)
592
+ );
593
+ }
594
+
595
+ function isFinalReportNode(node: WorkflowNode): boolean {
596
+ return node.id === 'final_report' || node.stage === 'final_report' || node.stage === 'report';
597
+ }
598
+
599
+ function workspaceNeedsHostPermissionBypass(target: ResolvedWorkspaceTarget): boolean {
600
+ return (target.kind === 'selected_project' || target.kind === 'custom') && target.projectPath !== target.appRoot;
601
+ }
602
+
603
+ function resolveNodePermissionMode(node: WorkflowNode, target: ResolvedWorkspaceTarget): string | undefined {
604
+ if (node.permissionMode && node.permissionMode !== 'default') {
605
+ return node.permissionMode;
606
+ }
607
+
608
+ if (workspaceNeedsHostPermissionBypass(target)) {
609
+ return 'bypassPermissions';
610
+ }
611
+
612
+ return node.permissionMode;
613
+ }
614
+
615
+ function buildPermissionFallbackOutput(
616
+ node: WorkflowNode,
617
+ reason: string,
618
+ target: ResolvedWorkspaceTarget,
619
+ ): string {
620
+ return [
621
+ 'Bu adım çalışma alanı izin sınırına takıldı.',
622
+ '',
623
+ `Ajan: ${node.agentLabel || node.id}`,
624
+ `Hedef çalışma alanı: ${target.projectPath}`,
625
+ `Hata: ${reason}`,
626
+ '',
627
+ 'Pixcode bu adımı workflow dışına taşırmadan devam ettirdi. Ajan aynı dış dizin yoluna tekrar tekrar erişmek yerine mevcut bağlamla ilerlemeli.',
628
+ ].join('\n');
629
+ }
630
+
631
+ function buildFallbackFinalReport(
632
+ outputs: Map<string, string>,
633
+ reason: string,
634
+ target: ResolvedWorkspaceTarget,
635
+ ): string {
636
+ const completedOutputs = [...outputs.entries()]
637
+ .map(([nodeId, output]) => [`## ${nodeId}`, output || '(çıktı yok)'].join('\n'))
638
+ .join('\n\n');
639
+
640
+ return [
641
+ 'Final rapor aracı çalışma alanı izin sınırına takıldı, bu yüzden Pixcode tamamlanan ajan çıktılarından güvenli bir özet üretti.',
642
+ '',
643
+ `Hedef çalışma alanı: ${target.projectPath}`,
644
+ `İzin hatası: ${reason}`,
645
+ '',
646
+ completedOutputs || 'Bu turda final rapora aktarılabilecek tamamlanmış ajan çıktısı yok.',
647
+ ].join('\n');
648
+ }
649
+
650
+ function completeNodeWithPermissionFallback(
651
+ nodeRun: WorkflowNodeRun,
652
+ node: WorkflowNode,
653
+ outputs: Map<string, string>,
654
+ completed: Set<string>,
655
+ reason: string,
656
+ target: ResolvedWorkspaceTarget,
657
+ ): void {
658
+ const outputText = isFinalReportNode(node)
659
+ ? buildFallbackFinalReport(outputs, reason, target)
660
+ : buildPermissionFallbackOutput(node, reason, target);
661
+
662
+ nodeRun.status = 'completed';
663
+ nodeRun.error = reason;
664
+ nodeRun.outputText = outputText;
665
+ nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
666
+ outputs.set(node.id, compactOutputForContext(outputText));
667
+ completed.add(node.id);
668
+ }
669
+
670
+ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
671
+ const agents = readAgentAssignments(metadata);
672
+ if (agents.length === 0) {
673
+ throw new Error('Select at least one CLI agent.');
674
+ }
675
+
676
+ const coordinator = agents.find((agent) => agent.adapterId === 'claude-code') ?? agents[0];
677
+ const roster = agentRoster(agents);
678
+ const workerSpecs = agents.map((agent, index) => ({
679
+ agent,
680
+ role: inferImplementationRole(agent),
681
+ stage: displayStage(agent, inferImplementationRole(agent)),
682
+ nodeId: safeAgentNodeId(agent, index, 'work'),
683
+ handoffNodeId: safeAgentNodeId(agent, index, 'handoff'),
684
+ }));
685
+ const backendHandoffNodeIds = workerSpecs
686
+ .filter((spec) => spec.role === 'backend')
687
+ .map((spec) => spec.handoffNodeId);
688
+ const implementationNodeIds = workerSpecs
689
+ .filter((spec) => spec.role !== 'review')
690
+ .map((spec) => spec.nodeId);
691
+ const handoffNodes: WorkflowNode[] = workerSpecs
692
+ .filter((spec) => spec.role === 'backend')
693
+ .map(({ agent, role, handoffNodeId }) => ({
694
+ id: handoffNodeId,
695
+ adapterId: agent.adapterId,
696
+ agentInstanceId: agent.instanceId,
697
+ agentLabel: `${agent.label} Handoff`,
698
+ assignment: agent.instruction,
699
+ stage: 'handoff',
700
+ model: agent.model,
701
+ permissionMode: agent.permissionMode,
702
+ toolsSettings: agent.toolsSettings,
703
+ prompt: handoffPrompt(agent, role),
704
+ inputs: ['coordinator'],
705
+ output: 'message',
706
+ onFail: 'continue',
707
+ timeoutMs: BACKEND_HANDOFF_TIMEOUT_MS,
708
+ }));
709
+ const workerNodes: WorkflowNode[] = workerSpecs.map(({ agent, role, stage, nodeId, handoffNodeId }) => {
710
+ const inputs = role === 'review'
711
+ ? (implementationNodeIds.length > 0 ? implementationNodeIds : ['coordinator'])
712
+ : role === 'frontend' && backendHandoffNodeIds.length > 0
713
+ ? ['coordinator', ...backendHandoffNodeIds]
714
+ : role === 'backend'
715
+ ? ['coordinator', handoffNodeId]
716
+ : ['coordinator'];
717
+
718
+ return {
719
+ id: nodeId,
720
+ adapterId: agent.adapterId,
721
+ agentInstanceId: agent.instanceId,
722
+ agentLabel: agent.label,
723
+ assignment: agent.instruction,
724
+ stage,
725
+ model: agent.model,
726
+ permissionMode: agent.permissionMode,
727
+ toolsSettings: agent.toolsSettings,
728
+ prompt: [
729
+ `You are ${agent.label} in a Pixcode CLI team.`,
730
+ `Your stage is: ${stage}.`,
731
+ stage !== role ? `Runtime routing category: ${role}.` : '',
732
+ 'The coordinator plan and any dependency outputs are included above. Use them together with the original user goal.',
733
+ agent.instruction
734
+ ? `Your explicit assignment from the user is: ${agent.instruction}`
735
+ : 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
736
+ rolePrompt(stage),
737
+ privacyGuardPrompt(),
738
+ 'Respond in the same language as the user request.',
739
+ ].filter(Boolean).join('\n'),
740
+ inputs,
741
+ output: 'both',
742
+ onFail: 'continue',
743
+ };
744
+ });
745
+
746
+ return {
747
+ ...workflow,
748
+ nodes: [
749
+ {
750
+ id: 'coordinator',
751
+ adapterId: coordinator.adapterId,
752
+ agentInstanceId: coordinator.instanceId,
753
+ agentLabel: coordinator.label,
754
+ stage: 'coordinator',
755
+ model: coordinator.model,
756
+ permissionMode: coordinator.permissionMode,
757
+ toolsSettings: coordinator.toolsSettings,
758
+ prompt: [
759
+ 'You are the coordinator for a Pixcode CLI agent team.',
760
+ 'Read the user goal, active CLI roster, and any per-agent assignments. Create a compact execution plan for the selected agents.',
761
+ 'If the user directly names a CLI, honor that. Do not invent permanent roles; assign work only from the goal, active agents, and explicit assignment text.',
762
+ `Active roster:\n${roster}`,
763
+ 'Respond in the same language as the user request.',
764
+ ].join('\n'),
765
+ inputs: [],
766
+ output: 'message',
767
+ onFail: 'abort',
768
+ },
769
+ ...handoffNodes,
770
+ ...workerNodes,
771
+ {
772
+ id: 'final_report',
773
+ adapterId: coordinator.adapterId,
774
+ agentInstanceId: coordinator.instanceId,
775
+ agentLabel: coordinator.label,
776
+ stage: 'final_report',
777
+ model: coordinator.model,
778
+ permissionMode: coordinator.permissionMode,
779
+ toolsSettings: coordinator.toolsSettings,
780
+ prompt: [
781
+ 'Collect the worker outputs into one user-facing result.',
782
+ 'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
783
+ 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
784
+ 'If a worker reveals internal process text, summarize only the useful user-facing result.',
785
+ 'Respond in the same language as the user request.',
786
+ ].join('\n'),
787
+ inputs: workerNodes.map((node) => node.id),
788
+ output: 'message',
789
+ onFail: 'abort',
790
+ },
791
+ ],
792
+ };
793
+ }
794
+
795
+ function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
796
+ return [
797
+ `You are ${agent.label} in a Pixcode decision workflow.`,
798
+ `Your stage is: ${stage}.`,
799
+ agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
800
+ agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
801
+ rolePrompt(stage),
802
+ privacyGuardPrompt(),
803
+ 'Keep the answer concise, structured, and useful for the next stage.',
804
+ 'Respond in the same language as the user request.',
805
+ ].filter(Boolean).join('\n');
806
+ }
807
+
808
+ function agentsWithRole(agents: AgentAssignment[], role: AgentRole): AgentAssignment[] {
809
+ return agents.filter((agent) => agent.role === role);
810
+ }
811
+
812
+ function autoAssignDebateAgents(agents: AgentAssignment[]): {
813
+ proposalAgents: AgentAssignment[];
814
+ critiqueAgents: AgentAssignment[];
815
+ responseAgents: AgentAssignment[];
816
+ reportAgent: AgentAssignment;
817
+ } {
818
+ const assigned = new Set<string>();
819
+ const markAssigned = (items: AgentAssignment[]) => {
820
+ for (const item of items) assigned.add(item.instanceId);
821
+ };
822
+ const pickNext = () =>
823
+ agents.find((agent) => !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report')
824
+ ?? agents.find((agent) => !assigned.has(agent.instanceId))
825
+ ?? agents[0];
826
+
827
+ const proposalAgents = agentsWithRole(agents, 'proposal');
828
+ if (proposalAgents.length === 0) proposalAgents.push(pickNext());
829
+ markAssigned(proposalAgents);
830
+
831
+ const critiqueAgents = agentsWithRole(agents, 'critique');
832
+ if (critiqueAgents.length === 0) critiqueAgents.push(pickNext());
833
+ markAssigned(critiqueAgents);
834
+
835
+ const responseAgents = agentsWithRole(agents, 'response');
836
+ if (responseAgents.length === 0 && agents.length > 2) {
837
+ responseAgents.push(...agents.filter((agent) =>
838
+ !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report',
839
+ ));
840
+ }
841
+ markAssigned(responseAgents);
842
+
843
+ const reportAgent = agentsWithRole(agents, 'decision')[0]
844
+ ?? agentsWithRole(agents, 'report')[0]
845
+ ?? agents[0];
846
+
847
+ return { proposalAgents, critiqueAgents, responseAgents, reportAgent };
848
+ }
849
+
850
+ function expandAdversarialDebateWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
851
+ const agents = readAgentAssignments(metadata);
852
+ if (agents.length === 0) {
853
+ throw new Error('Select at least one CLI agent.');
854
+ }
855
+
856
+ const {
857
+ proposalAgents,
858
+ critiqueAgents,
859
+ responseAgents,
860
+ reportAgent,
861
+ } = autoAssignDebateAgents(agents);
862
+
863
+ const proposalNodes: WorkflowNode[] = proposalAgents.map((agent, index) => ({
864
+ id: safeAgentNodeId(agent, index, 'proposal'),
865
+ adapterId: agent.adapterId,
866
+ agentInstanceId: agent.instanceId,
867
+ agentLabel: agent.label,
868
+ assignment: agent.instruction || 'Proposal stage',
869
+ stage: 'proposal',
870
+ model: agent.model,
871
+ permissionMode: agent.permissionMode,
872
+ toolsSettings: agent.toolsSettings,
873
+ prompt: stagePrompt(agent, 'proposal'),
874
+ inputs: [],
875
+ output: 'message',
876
+ onFail: 'continue',
877
+ }));
878
+ const critiqueNodes: WorkflowNode[] = critiqueAgents.map((agent, index) => ({
879
+ id: safeAgentNodeId(agent, index, 'critique'),
880
+ adapterId: agent.adapterId,
881
+ agentInstanceId: agent.instanceId,
882
+ agentLabel: agent.label,
883
+ assignment: agent.instruction || 'Critique stage',
884
+ stage: 'critique',
885
+ model: agent.model,
886
+ permissionMode: agent.permissionMode,
887
+ toolsSettings: agent.toolsSettings,
888
+ prompt: stagePrompt(agent, 'critique'),
889
+ inputs: proposalNodes.map((node) => node.id),
890
+ output: 'message',
891
+ onFail: 'continue',
892
+ }));
893
+ const responseNodes: WorkflowNode[] = responseAgents.map((agent, index) => ({
894
+ id: safeAgentNodeId(agent, index, 'response'),
895
+ adapterId: agent.adapterId,
896
+ agentInstanceId: agent.instanceId,
897
+ agentLabel: agent.label,
898
+ assignment: agent.instruction || 'Response stage',
899
+ stage: 'response',
900
+ model: agent.model,
901
+ permissionMode: agent.permissionMode,
902
+ toolsSettings: agent.toolsSettings,
903
+ prompt: stagePrompt(agent, 'response'),
904
+ inputs: critiqueNodes.map((node) => node.id),
905
+ output: 'message',
906
+ onFail: 'continue',
907
+ }));
908
+ const finalInputs = responseNodes.length > 0
909
+ ? responseNodes.map((node) => node.id)
910
+ : critiqueNodes.map((node) => node.id);
911
+
912
+ return {
913
+ ...workflow,
914
+ nodes: [
915
+ ...proposalNodes,
916
+ ...critiqueNodes,
917
+ ...responseNodes,
918
+ {
919
+ id: 'final_report',
920
+ adapterId: reportAgent.adapterId,
921
+ agentInstanceId: reportAgent.instanceId,
922
+ agentLabel: reportAgent.label,
923
+ assignment: reportAgent.instruction || 'Final decision report',
924
+ stage: 'final_report',
925
+ model: reportAgent.model,
926
+ permissionMode: reportAgent.permissionMode,
927
+ toolsSettings: reportAgent.toolsSettings,
928
+ prompt: [
929
+ 'Produce the final decision report from the debate.',
930
+ 'Use this exact structure:',
931
+ '1. Short decision',
932
+ '2. Why',
933
+ '3. Risks',
934
+ '4. Suggested next prompt',
935
+ '5. Proposed agent team and assignments',
936
+ 'The next prompt should be ready to paste into Pixcode Agent Team mode.',
937
+ 'Do not edit files. Respond in the same language as the user request.',
938
+ ].join('\n'),
939
+ inputs: finalInputs,
940
+ output: 'message',
941
+ onFail: 'abort',
942
+ },
943
+ ],
944
+ };
945
+ }
946
+
947
+ function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
948
+ const agents = readAgentAssignments(metadata);
949
+ if (agents.length === 0) {
950
+ throw new Error('Select at least one CLI agent.');
951
+ }
952
+
953
+ const nodes: WorkflowNode[] = agents.flatMap((agent, index): WorkflowNode[] => {
954
+ const initNodeId = safeAgentNodeId(agent, index, 'init');
955
+ const workNodeId = safeAgentNodeId(agent, index, 'work');
956
+ const compactNodeId = safeAgentNodeId(agent, index, 'compact');
957
+
958
+ return [
959
+ {
960
+ id: initNodeId,
961
+ adapterId: agent.adapterId,
962
+ agentInstanceId: agent.instanceId,
963
+ agentLabel: `${agent.label} Init`,
964
+ assignment: agent.instruction,
965
+ stage: 'handoff_init',
966
+ model: agent.model,
967
+ permissionMode: agent.permissionMode,
968
+ toolsSettings: agent.toolsSettings,
969
+ prompt: handoffInitPrompt(agent, index),
970
+ inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'compact')],
971
+ output: 'message',
972
+ onFail: 'abort',
973
+ internal: true,
974
+ },
975
+ {
976
+ id: workNodeId,
977
+ adapterId: agent.adapterId,
978
+ agentInstanceId: agent.instanceId,
979
+ agentLabel: agent.label,
980
+ assignment: agent.instruction,
981
+ stage: agent.role ?? 'implementation',
982
+ model: agent.model,
983
+ permissionMode: agent.permissionMode,
984
+ toolsSettings: agent.toolsSettings,
985
+ prompt: handoffWorkPrompt(agent, index),
986
+ inputs: [initNodeId],
987
+ output: 'both',
988
+ onFail: 'abort',
989
+ },
990
+ {
991
+ id: compactNodeId,
992
+ adapterId: agent.adapterId,
993
+ agentInstanceId: agent.instanceId,
994
+ agentLabel: `${agent.label} Compact`,
995
+ assignment: agent.instruction,
996
+ stage: 'handoff_compact',
997
+ model: agent.model,
998
+ permissionMode: agent.permissionMode,
999
+ toolsSettings: agent.toolsSettings,
1000
+ prompt: handoffCompactPrompt(agent, index),
1001
+ inputs: [workNodeId],
1002
+ output: 'message',
1003
+ onFail: 'abort',
1004
+ internal: true,
1005
+ },
1006
+ ];
1007
+ });
1008
+ const reportAgent = agents[0];
1009
+ const lastCompactNodeId = safeAgentNodeId(agents[agents.length - 1], agents.length - 1, 'compact');
1010
+
1011
+ return {
1012
+ ...workflow,
1013
+ nodes: [
1014
+ ...nodes,
1015
+ {
1016
+ id: 'final_report',
1017
+ adapterId: reportAgent.adapterId,
1018
+ agentInstanceId: reportAgent.instanceId,
1019
+ agentLabel: reportAgent.label,
1020
+ stage: 'final_report',
1021
+ model: reportAgent.model,
1022
+ permissionMode: reportAgent.permissionMode,
1023
+ toolsSettings: reportAgent.toolsSettings,
1024
+ prompt: [
1025
+ 'Create the final user-facing result for this strict handoff run.',
1026
+ 'Use the final compact handoff packet and the original user goal.',
1027
+ 'Summarize what each visible agent did, what changed, verification, blockers, and next actions.',
1028
+ 'Do not expose internal init packets, compact packets, prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
1029
+ 'Respond in the same language as the user request.',
1030
+ ].join('\n'),
1031
+ inputs: [lastCompactNodeId],
1032
+ output: 'message',
1033
+ onFail: 'abort',
1034
+ },
1035
+ ],
1036
+ };
1037
+ }
1038
+
1039
+ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
1040
+ if (workflow.id === 'agent_team') {
1041
+ return expandAgentTeamWorkflow(workflow, metadata);
1042
+ }
1043
+
1044
+ const agents = readAgentAssignments(metadata);
1045
+ if (workflow.id === 'adversarial_debate') {
1046
+ return expandAdversarialDebateWorkflow(workflow, metadata);
1047
+ }
1048
+ if (workflow.id === 'sequential_handoff') {
1049
+ return expandSequentialHandoffWorkflow(workflow, metadata);
1050
+ }
1051
+ if (workflow.id !== 'multi_model_review' || agents.length === 0) {
1052
+ return workflow;
1053
+ }
1054
+
1055
+ const reportAgent = agentsWithRole(agents, 'report')[0] ?? agentsWithRole(agents, 'decision')[0] ?? agents[0];
1056
+ const reviewAgents = agents.filter((agent) => agent.instanceId !== reportAgent.instanceId || agents.length === 1);
1057
+ const reviewNodes: WorkflowNode[] = reviewAgents.map((agent, index) => ({
1058
+ id: safeAgentNodeId(agent, index, 'review'),
1059
+ adapterId: agent.adapterId,
1060
+ agentInstanceId: agent.instanceId,
1061
+ agentLabel: agent.label,
1062
+ assignment: agent.instruction,
1063
+ stage: 'review',
1064
+ model: agent.model,
1065
+ permissionMode: agent.permissionMode,
1066
+ toolsSettings: agent.toolsSettings,
1067
+ prompt: [
1068
+ `You are ${agent.label}.`,
1069
+ 'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
1070
+ agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
1071
+ privacyGuardPrompt(),
1072
+ 'Respond in the same language as the user request.',
1073
+ ].filter(Boolean).join('\n'),
1074
+ inputs: [],
1075
+ output: 'both',
1076
+ onFail: 'continue',
1077
+ }));
1078
+
1079
+ return {
1080
+ ...workflow,
1081
+ nodes: [
1082
+ ...reviewNodes,
1083
+ {
1084
+ id: 'aggregate',
1085
+ adapterId: reportAgent.adapterId,
1086
+ agentInstanceId: reportAgent.instanceId,
1087
+ agentLabel: reportAgent.label,
1088
+ stage: 'report',
1089
+ model: reportAgent.model,
1090
+ permissionMode: reportAgent.permissionMode,
1091
+ toolsSettings: reportAgent.toolsSettings,
1092
+ prompt: [
1093
+ 'Aggregate the prior agent reviews into a concise prioritized report.',
1094
+ 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
1095
+ 'Respond in the same language as the user request.',
1096
+ ].join('\n'),
1097
+ inputs: reviewNodes.map((node) => node.id),
1098
+ output: 'message',
1099
+ onFail: 'abort',
1100
+ },
1101
+ ],
1102
+ };
1103
+ }
1104
+
1105
+ async function cancelHermesTask(taskId: string): Promise<void> {
1106
+ await fetch(`${localHermesBaseUrl()}/tasks/${taskId}/cancel`, { method: 'POST' }).catch(() => undefined);
1107
+ }
1108
+
1109
+ function readTaskResult(task: RawTask): TaskResult {
1110
+ const messages = (task.history ?? []).map((message) => ({
1111
+ role: typeof message.role === 'string' ? message.role : 'agent',
1112
+ text: (message.parts ?? [])
1113
+ .filter((part) => part.kind === 'text' && typeof part.text === 'string')
1114
+ .map((part) => part.text)
1115
+ .join('\n'),
1116
+ })).filter((message) => message.text.trim());
1117
+ const artifacts = (task.artifacts ?? []).map((artifact) => {
1118
+ const text = (artifact.parts ?? [])
1119
+ .filter((part) => part.kind === 'text' && typeof part.text === 'string')
1120
+ .map((part) => part.text)
1121
+ .join('\n');
1122
+ const data = (artifact.parts ?? []).find((part) => part.kind === 'data')?.data;
1123
+ return {
1124
+ type: artifact.type ?? 'data',
1125
+ text: text || undefined,
1126
+ data,
1127
+ metadata: artifact.metadata,
1128
+ };
1129
+ });
1130
+ const outputMessages = messages.filter((message) => message.role !== 'user');
1131
+ const userFacingTaskText = outputMessages.map((message) => message.text.trim()).filter(Boolean).join('\n\n');
1132
+ const error = task.error?.message
1133
+ ? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
1134
+ : undefined;
1135
+ return {
1136
+ state: task.state ?? 'submitted',
1137
+ text: userFacingTaskText,
1138
+ error,
1139
+ messages,
1140
+ artifacts,
1141
+ };
1142
+ }
1143
+
1144
+ async function waitForTask(
1145
+ taskId: string,
1146
+ shouldCancel?: () => boolean,
1147
+ onSnapshot?: (result: TaskResult) => void,
1148
+ timeoutMs?: number,
1149
+ ): Promise<TaskResult> {
1150
+ const timeout = timeoutMs && timeoutMs > 0 ? timeoutMs : undefined;
1151
+ const deadline = timeout ? Date.now() + timeout : undefined;
1152
+ for (;;) {
1153
+ if (shouldCancel?.()) {
1154
+ throw new WorkflowCanceledError();
1155
+ }
1156
+ if (deadline && Date.now() >= deadline) {
1157
+ throw new WorkflowNodeTimeoutError(timeout ?? 0);
1158
+ }
1159
+ const response = await fetch(`${localHermesBaseUrl()}/tasks/${taskId}`);
1160
+ const task = await response.json() as RawTask;
1161
+ const snapshot = readTaskResult(task);
1162
+ onSnapshot?.(snapshot);
1163
+ if (task.state && TERMINAL.has(task.state)) {
1164
+ return snapshot;
1165
+ }
1166
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1167
+ }
1168
+ }
1169
+
1170
+ function readyNodes(workflow: Workflow, completed: Set<string>, started: Set<string>): WorkflowNode[] {
1171
+ return workflow.nodes.filter((node) =>
1172
+ !started.has(node.id) && node.inputs.every((input) => completed.has(input)),
1173
+ );
1174
+ }
1175
+
1176
+ function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
1177
+ return {
1178
+ nodeId: node.id,
1179
+ adapterId: node.adapterId,
1180
+ agentInstanceId: node.agentInstanceId,
1181
+ agentLabel: node.agentLabel,
1182
+ assignment: node.assignment,
1183
+ promptPreview: node.prompt,
1184
+ model: node.model,
1185
+ permissionMode: node.permissionMode,
1186
+ timeoutMs: node.timeoutMs,
1187
+ stage: node.stage,
1188
+ internal: node.internal,
1189
+ fallbackTrigger: node.fallbackTrigger,
1190
+ fallbackSourceNodeId: node.fallbackSourceNodeId,
1191
+ status: 'queued',
1192
+ };
1193
+ }
1194
+
1195
+ function uniqueInputs(inputs: string[]): string[] {
1196
+ return [...new Set(inputs.filter(Boolean))];
1197
+ }
1198
+
1199
+ function isReviewNode(node: WorkflowNode): boolean {
1200
+ return node.stage === 'review';
1201
+ }
1202
+
1203
+ function isImplementationNode(node: WorkflowNode): boolean {
1204
+ return node.stage === 'backend' || node.stage === 'frontend' || node.stage === 'implementation' || node.stage === 'repair';
1205
+ }
1206
+
1207
+ function reviewRequiresRepair(text: string): boolean {
1208
+ const normalized = text.toLocaleLowerCase('tr').replace(/\s+/g, ' ').trim();
1209
+ if (!normalized) return false;
1210
+
1211
+ const approvalPatterns = [
1212
+ /hata yok/u,
1213
+ /sorun yok/u,
1214
+ /problem yok/u,
1215
+ /bulgu yok/u,
1216
+ /kritik bulgu yok/u,
1217
+ /temiz/u,
1218
+ /onaylı/u,
1219
+ /onayli/u,
1220
+ /approved/u,
1221
+ /lgtm/u,
1222
+ /no issues/u,
1223
+ /no findings/u,
1224
+ /looks good/u,
1225
+ /pass(?:ed)?/u,
1226
+ ];
1227
+ const actionableText = approvalPatterns.reduce((current, pattern) => current.replace(pattern, ' '), normalized);
1228
+ const issuePatterns = [
1229
+ /hata/u,
1230
+ /bug/u,
1231
+ /kritik/u,
1232
+ /critical/u,
1233
+ /blocker/u,
1234
+ /regression/u,
1235
+ /failed/u,
1236
+ /failure/u,
1237
+ /fail/u,
1238
+ /eksik/u,
1239
+ /düzelt/u,
1240
+ /duzelt/u,
1241
+ /fix required/u,
1242
+ /needs fix/u,
1243
+ /sorun/u,
1244
+ /risk/u,
1245
+ /güvenlik/u,
1246
+ /guvenlik/u,
1247
+ /security/u,
1248
+ /çalışmıyor/u,
1249
+ /calismiyor/u,
1250
+ ];
1251
+
1252
+ return issuePatterns.some((pattern) => pattern.test(actionableText));
1253
+ }
1254
+
1255
+ function findRepairFixer(workflow: Workflow, reviewNode: WorkflowNode): WorkflowNode | undefined {
1256
+ return reviewNode.inputs
1257
+ .map((input) => workflow.nodes.find((node) => node.id === input))
1258
+ .find((node): node is WorkflowNode => Boolean(node && isImplementationNode(node)))
1259
+ ?? workflow.nodes.find((node) => isImplementationNode(node))
1260
+ ?? workflow.nodes.find((node) => node.stage === 'coordinator');
1261
+ }
1262
+
1263
+ class WorkflowRunner {
1264
+ private readonly cancelingRuns = new Set<string>();
1265
+
1266
+ preview(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
1267
+ const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
1268
+ validateWorkflow(runtimeWorkflow);
1269
+ return runtimeWorkflow;
1270
+ }
1271
+
1272
+ start(workflow: Workflow, input = '', metadata?: Record<string, unknown>): WorkflowRun {
1273
+ const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
1274
+ validateWorkflow(runtimeWorkflow);
1275
+ const workspaceTarget = resolveWorkflowWorkspace(metadata);
1276
+ const permissionPolicy = resolvePermissionPolicyFromMetadata(metadata);
1277
+ const runMetadata: Record<string, unknown> = {
1278
+ ...metadata,
1279
+ permissionPolicy,
1280
+ projectPath: workspaceTarget.projectPath,
1281
+ selectedProjectPath: workspaceTarget.selectedProjectPath,
1282
+ workspaceTarget: workspaceTargetMetadata(workspaceTarget),
1283
+ };
1284
+ const run: WorkflowRun = {
1285
+ id: newId('wrun'),
1286
+ workflowId: runtimeWorkflow.id,
1287
+ contextId: newId('ctx'),
1288
+ status: 'queued',
1289
+ input,
1290
+ nodeRuns: runtimeWorkflow.nodes.map(nodeRunFromNode),
1291
+ startedAt: Date.now(),
1292
+ metadata: runMetadata,
1293
+ };
1294
+ workflowStore.setRun(run);
1295
+ const orchestrationTaskId = readString(runMetadata.orchestrationTaskId);
1296
+ if (orchestrationTaskId) {
1297
+ orchestrationTaskService.linkWorkflowRun(orchestrationTaskId, run);
1298
+ }
1299
+ void this.execute(runtimeWorkflow, run);
1300
+ return run;
1301
+ }
1302
+
1303
+ async cancel(runId: string): Promise<WorkflowRun | undefined> {
1304
+ const run = workflowStore.getRun(runId);
1305
+ if (!run) return undefined;
1306
+ if (TERMINAL.has(run.status)) return run;
1307
+
1308
+ this.cancelingRuns.add(run.id);
1309
+ const taskIds = run.nodeRuns
1310
+ .filter((node) => node.hermesTaskId && (node.status === 'running' || node.status === 'queued'))
1311
+ .map((node) => node.hermesTaskId as string);
1312
+
1313
+ this.markCanceled(run);
1314
+ workflowStore.setRun(run);
1315
+
1316
+ await Promise.all(taskIds.map((taskId) => cancelHermesTask(taskId)));
1317
+
1318
+ return workflowStore.getRun(run.id) ?? run;
1319
+ }
1320
+
1321
+ private isCanceling(runId: string): boolean {
1322
+ return this.cancelingRuns.has(runId) || workflowStore.getRun(runId)?.status === 'canceled';
1323
+ }
1324
+
1325
+ private markCanceled(run: WorkflowRun): void {
1326
+ run.status = 'canceled';
1327
+ run.finishedAt = run.finishedAt ?? Date.now();
1328
+ for (const nodeRun of run.nodeRuns) {
1329
+ if (!TERMINAL.has(nodeRun.status) && nodeRun.status !== SKIPPED) {
1330
+ nodeRun.status = 'canceled';
1331
+ nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ private fallbackAgentFor(run: WorkflowRun, node: WorkflowNode): AgentAssignment | undefined {
1337
+ if (node.stage === 'fallback' || node.id.startsWith('fallback_')) {
1338
+ return undefined;
1339
+ }
1340
+
1341
+ const settings = getMetadataRecord(run.metadata, 'settings');
1342
+ const fallbackAgentInstanceId = readString(settings.fallbackAgentInstanceId);
1343
+ if (!fallbackAgentInstanceId || fallbackAgentInstanceId === node.agentInstanceId) {
1344
+ return undefined;
1345
+ }
1346
+
1347
+ return readAgentAssignments(run.metadata).find((agent) => agent.instanceId === fallbackAgentInstanceId);
1348
+ }
1349
+
1350
+ private createFallbackNode(
1351
+ node: WorkflowNode,
1352
+ fallbackAgent: AgentAssignment,
1353
+ reason: string,
1354
+ fallbackTrigger: WorkflowFallbackTrigger,
1355
+ ): WorkflowNode {
1356
+ const fallbackSuffix = safeNodeId(fallbackAgent.instanceId, 'fallback');
1357
+ return {
1358
+ ...node,
1359
+ id: `fallback_${node.id}_${fallbackSuffix}`,
1360
+ adapterId: fallbackAgent.adapterId,
1361
+ agentInstanceId: fallbackAgent.instanceId,
1362
+ agentLabel: `${fallbackAgent.label} Fallback`,
1363
+ assignment: `Fallback for ${node.agentLabel || node.id}`,
1364
+ stage: 'fallback',
1365
+ model: fallbackAgent.model,
1366
+ permissionMode: fallbackAgent.permissionMode,
1367
+ toolsSettings: fallbackAgent.toolsSettings,
1368
+ fallbackTrigger,
1369
+ fallbackSourceNodeId: node.id,
1370
+ prompt: [
1371
+ 'The previous CLI agent failed on this orchestration step.',
1372
+ `Failed step: ${node.agentLabel || node.id}`,
1373
+ `Fallback trigger: ${fallbackTrigger}`,
1374
+ `Failure: ${reason}`,
1375
+ 'Take over the same assignment as the backup CLI. Use the original goal and upstream context.',
1376
+ 'Do not repeat unrelated work; complete the failed step and report what you did.',
1377
+ node.prompt,
1378
+ ].join('\n'),
1379
+ onFail: 'continue',
1380
+ };
1381
+ }
1382
+
1383
+ private recordFallbackSkipped(
1384
+ run: WorkflowRun,
1385
+ node: WorkflowNode,
1386
+ reason: string,
1387
+ fallbackTrigger: WorkflowFallbackTrigger,
1388
+ skippedReason: string,
1389
+ ): void {
1390
+ const fallbackSkippedEvents = Array.isArray(run.metadata?.fallbackSkippedEvents)
1391
+ ? run.metadata.fallbackSkippedEvents
1392
+ : [];
1393
+ run.metadata = {
1394
+ ...run.metadata,
1395
+ fallbackSkippedEvents: [
1396
+ ...fallbackSkippedEvents,
1397
+ {
1398
+ nodeId: node.id,
1399
+ trigger: fallbackTrigger,
1400
+ reason,
1401
+ skippedReason,
1402
+ createdAt: Date.now(),
1403
+ },
1404
+ ],
1405
+ };
1406
+ workflowStore.setRun(run);
1407
+ }
1408
+
1409
+ private async runFallbackAfterFailure(
1410
+ node: WorkflowNode,
1411
+ workflow: Workflow,
1412
+ run: WorkflowRun,
1413
+ outputs: Map<string, string>,
1414
+ started: Set<string>,
1415
+ completed: Set<string>,
1416
+ reason: string,
1417
+ trigger?: WorkflowFallbackTrigger,
1418
+ ): Promise<boolean> {
1419
+ const fallbackTrigger = classifyWorkflowFailure(reason, trigger);
1420
+ const fallbackAgent = this.fallbackAgentFor(run, node);
1421
+ if (!fallbackAgent) {
1422
+ this.recordFallbackSkipped(run, node, reason, fallbackTrigger, 'No fallback agent is configured for this run.');
1423
+ return false;
1424
+ }
1425
+ const decision = resolveWorkflowFallbackDecision({
1426
+ run,
1427
+ node,
1428
+ reason,
1429
+ trigger: fallbackTrigger,
1430
+ fallbackAgentInstanceId: fallbackAgent.instanceId,
1431
+ });
1432
+ if (!decision.shouldFallback) {
1433
+ this.recordFallbackSkipped(
1434
+ run,
1435
+ node,
1436
+ reason,
1437
+ decision.trigger,
1438
+ decision.skippedReason ?? 'Fallback policy skipped this failure.',
1439
+ );
1440
+ return false;
1441
+ }
1442
+ if (workflow.nodes.length + 1 > 64) {
1443
+ run.metadata = {
1444
+ ...run.metadata,
1445
+ fallbackSkipped: `Workflow node limit reached after ${node.id}.`,
1446
+ };
1447
+ workflowStore.setRun(run);
1448
+ return false;
1449
+ }
1450
+
1451
+ let fallbackNode = this.createFallbackNode(node, fallbackAgent, reason, decision.trigger);
1452
+ let collision = 1;
1453
+ while (workflow.nodes.some((candidate) => candidate.id === fallbackNode.id)) {
1454
+ collision += 1;
1455
+ fallbackNode = {
1456
+ ...fallbackNode,
1457
+ id: `${fallbackNode.id}_${collision}`,
1458
+ };
1459
+ }
1460
+
1461
+ const nodeIndex = workflow.nodes.findIndex((candidate) => candidate.id === node.id);
1462
+ const runIndex = run.nodeRuns.findIndex((candidate) => candidate.nodeId === node.id);
1463
+ if (nodeIndex >= 0) {
1464
+ workflow.nodes.splice(nodeIndex + 1, 0, fallbackNode);
1465
+ } else {
1466
+ workflow.nodes.push(fallbackNode);
1467
+ }
1468
+ if (runIndex >= 0) {
1469
+ run.nodeRuns.splice(runIndex + 1, 0, nodeRunFromNode(fallbackNode));
1470
+ } else {
1471
+ run.nodeRuns.push(nodeRunFromNode(fallbackNode));
1472
+ }
1473
+
1474
+ const fallbackEvents = Array.isArray(run.metadata?.fallbackEvents)
1475
+ ? run.metadata.fallbackEvents
1476
+ : [];
1477
+ run.metadata = {
1478
+ ...run.metadata,
1479
+ fallbackEvents: [
1480
+ ...fallbackEvents,
1481
+ {
1482
+ nodeId: node.id,
1483
+ fallbackNodeId: fallbackNode.id,
1484
+ fallbackAgentInstanceId: fallbackAgent.instanceId,
1485
+ trigger: decision.trigger,
1486
+ policy: decision.policy,
1487
+ reason,
1488
+ startedAt: Date.now(),
1489
+ },
1490
+ ],
1491
+ };
1492
+ workflowStore.setRun(run);
1493
+
1494
+ await this.executeNode(fallbackNode, workflow, run, outputs, started, completed);
1495
+
1496
+ const fallbackRun = run.nodeRuns.find((candidate) => candidate.nodeId === fallbackNode.id);
1497
+ if (fallbackRun?.status !== 'completed') {
1498
+ return false;
1499
+ }
1500
+
1501
+ const fallbackOutput = outputs.get(fallbackNode.id) || fallbackRun.outputText;
1502
+ if (fallbackOutput) {
1503
+ outputs.set(node.id, compactOutputForContext(fallbackOutput));
1504
+ }
1505
+ completed.add(node.id);
1506
+ workflowStore.setRun(run);
1507
+ return true;
1508
+ }
1509
+
1510
+ private maybeAddRepairCycle(
1511
+ node: WorkflowNode,
1512
+ workflow: Workflow,
1513
+ run: WorkflowRun,
1514
+ result: TaskResult,
1515
+ ): void {
1516
+ if (workflow.id !== 'agent_team') return;
1517
+ if (!isReviewNode(node) || node.id.startsWith('repair_') || node.id.startsWith('recheck_')) return;
1518
+ if (!reviewRequiresRepair(`${result.text}\n${result.error ?? ''}`)) return;
1519
+
1520
+ const maxRepairCycles = readMaxRepairCycles(run.metadata);
1521
+ if (maxRepairCycles <= 0) return;
1522
+
1523
+ const existingCycles = workflow.nodes.filter((candidate) => candidate.id.startsWith(`repair_${node.id}_`)).length;
1524
+ if (existingCycles >= maxRepairCycles) return;
1525
+
1526
+ if (workflow.nodes.length + 2 > 64) {
1527
+ run.metadata = {
1528
+ ...run.metadata,
1529
+ dynamicRepairSkipped: `Workflow node limit reached after ${node.id}.`,
1530
+ };
1531
+ workflowStore.setRun(run);
1532
+ return;
1533
+ }
1534
+
1535
+ const fixer = findRepairFixer(workflow, node);
1536
+ if (!fixer || fixer.id === node.id) return;
1537
+
1538
+ const cycle = existingCycles + 1;
1539
+ const repairNode: WorkflowNode = {
1540
+ id: `repair_${node.id}_${cycle}`,
1541
+ adapterId: fixer.adapterId,
1542
+ agentInstanceId: fixer.agentInstanceId,
1543
+ agentLabel: fixer.agentLabel ? `${fixer.agentLabel} Repair` : undefined,
1544
+ assignment: `Automatic repair from ${node.agentLabel || node.id} review findings`,
1545
+ stage: 'repair',
1546
+ model: fixer.model,
1547
+ permissionMode: fixer.permissionMode,
1548
+ toolsSettings: fixer.toolsSettings,
1549
+ prompt: [
1550
+ 'A review stage found actionable issues in the prior work.',
1551
+ 'Use the original user goal, prior implementation outputs, and review output included above.',
1552
+ 'Fix only the reported issues; do not restart the whole project or duplicate unrelated work.',
1553
+ 'Report changed files, commands, verification, and any remaining blockers.',
1554
+ 'Respond in the same language as the user request.',
1555
+ ].join('\n'),
1556
+ inputs: uniqueInputs([...node.inputs, fixer.id, node.id]),
1557
+ output: 'both',
1558
+ onFail: 'continue',
1559
+ };
1560
+ const recheckNode: WorkflowNode = {
1561
+ id: `recheck_${node.id}_${cycle}`,
1562
+ adapterId: node.adapterId,
1563
+ agentInstanceId: node.agentInstanceId,
1564
+ agentLabel: node.agentLabel ? `${node.agentLabel} Recheck` : undefined,
1565
+ assignment: 'Automatic validation after repair',
1566
+ stage: 'review',
1567
+ model: node.model,
1568
+ permissionMode: node.permissionMode,
1569
+ toolsSettings: node.toolsSettings,
1570
+ prompt: [
1571
+ 'Validate the automatic repair against the original review findings.',
1572
+ 'Approve only if the reported issues are fixed.',
1573
+ 'If anything remains, list the remaining blockers clearly and do not invent new unrelated scope.',
1574
+ 'Respond in the same language as the user request.',
1575
+ ].join('\n'),
1576
+ inputs: uniqueInputs([node.id, repairNode.id]),
1577
+ output: 'message',
1578
+ onFail: 'continue',
1579
+ };
1580
+
1581
+ const finalIndex = workflow.nodes.findIndex((candidate) =>
1582
+ candidate.id === 'final_report' || candidate.stage === 'final_report' || candidate.stage === 'report',
1583
+ );
1584
+ if (finalIndex >= 0) {
1585
+ workflow.nodes.splice(finalIndex, 0, repairNode, recheckNode);
1586
+ run.nodeRuns.splice(finalIndex, 0, nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
1587
+ } else {
1588
+ workflow.nodes.push(repairNode, recheckNode);
1589
+ run.nodeRuns.push(nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
1590
+ }
1591
+
1592
+ for (const finalNode of workflow.nodes) {
1593
+ if (finalNode.id === 'final_report' || finalNode.stage === 'final_report' || finalNode.stage === 'report') {
1594
+ finalNode.inputs = uniqueInputs([...finalNode.inputs, recheckNode.id]);
1595
+ }
1596
+ }
1597
+
1598
+ const repairCycles = Array.isArray(run.metadata?.dynamicRepairCycles)
1599
+ ? run.metadata.dynamicRepairCycles
1600
+ : [];
1601
+ run.metadata = {
1602
+ ...run.metadata,
1603
+ dynamicRepairCycles: [
1604
+ ...repairCycles,
1605
+ {
1606
+ reviewNodeId: node.id,
1607
+ repairNodeId: repairNode.id,
1608
+ recheckNodeId: recheckNode.id,
1609
+ fixerNodeId: fixer.id,
1610
+ },
1611
+ ],
1612
+ };
1613
+ workflowStore.setRun(run);
1614
+ }
1615
+
1616
+ private async execute(workflow: Workflow, run: WorkflowRun): Promise<void> {
1617
+ run.status = 'running';
1618
+ workflowStore.setRun(run);
1619
+ const completed = new Set<string>();
1620
+ const started = new Set<string>();
1621
+ const outputs = new Map<string, string>();
1622
+ const maxParallelAgents = readMaxParallelAgents(run.metadata);
1623
+
1624
+ try {
1625
+ while (completed.size < workflow.nodes.length) {
1626
+ if (this.isCanceling(run.id)) {
1627
+ throw new WorkflowCanceledError();
1628
+ }
1629
+ const batch = readyNodes(workflow, completed, started);
1630
+ if (batch.length === 0) {
1631
+ throw new Error('Workflow stalled; no ready nodes remain.');
1632
+ }
1633
+ for (let index = 0; index < batch.length; index += maxParallelAgents) {
1634
+ if (this.isCanceling(run.id)) {
1635
+ throw new WorkflowCanceledError();
1636
+ }
1637
+ const slice = batch.slice(index, index + maxParallelAgents);
1638
+ await Promise.all(slice.map((node) => this.executeNode(node, workflow, run, outputs, started, completed)));
1639
+ }
1640
+ }
1641
+ if (this.isCanceling(run.id)) {
1642
+ throw new WorkflowCanceledError();
1643
+ }
1644
+ run.status = 'completed';
1645
+ } catch (error) {
1646
+ if (error instanceof WorkflowCanceledError || this.isCanceling(run.id)) {
1647
+ this.markCanceled(run);
1648
+ } else {
1649
+ run.status = 'failed';
1650
+ run.metadata = {
1651
+ ...run.metadata,
1652
+ error: error instanceof Error ? error.message : String(error),
1653
+ };
1654
+ }
1655
+ } finally {
1656
+ run.finishedAt = run.finishedAt ?? Date.now();
1657
+ workflowStore.setRun(run);
1658
+ orchestrationTaskService.updateFromWorkflowRun(run);
1659
+ notifyWorkflowRunFinished(run);
1660
+ const webhookRunStatus = String(run.status);
1661
+ dispatchWebhookEvent({
1662
+ type: webhookRunStatus === 'completed'
1663
+ ? 'run.completed'
1664
+ : webhookRunStatus === 'canceled'
1665
+ ? 'run.canceled'
1666
+ : 'run.failed',
1667
+ payload: {
1668
+ runId: run.id,
1669
+ workflowId: run.workflowId,
1670
+ status: webhookRunStatus,
1671
+ error: readString(run.metadata?.error),
1672
+ },
1673
+ });
1674
+ this.cancelingRuns.delete(run.id);
1675
+ }
1676
+ }
1677
+
1678
+ private recordPermissionDecision(
1679
+ run: WorkflowRun,
1680
+ nodeRun: WorkflowNodeRun,
1681
+ decision: PermissionDecision,
1682
+ ): void {
1683
+ nodeRun.permissionDecisions = [
1684
+ ...(nodeRun.permissionDecisions ?? []),
1685
+ decision,
1686
+ ];
1687
+
1688
+ const existingApprovals = permissionApprovalRequests(run)
1689
+ .filter((approval) => approval.id !== decision.approvalRequest?.id);
1690
+ run.metadata = {
1691
+ ...run.metadata,
1692
+ permissionPolicyEvents: [
1693
+ ...permissionPolicyEvents(run),
1694
+ decision.event,
1695
+ ],
1696
+ pendingPermissionApprovals: decision.approvalRequest
1697
+ ? [
1698
+ ...existingApprovals,
1699
+ decision.approvalRequest,
1700
+ ]
1701
+ : existingApprovals,
1702
+ };
1703
+
1704
+ if (decision.approvalRequest) {
1705
+ notifyPermissionApprovalRequested(run, decision);
1706
+ dispatchWebhookEvent({
1707
+ type: 'approval.needed',
1708
+ payload: {
1709
+ runId: run.id,
1710
+ workflowId: run.workflowId,
1711
+ approvalId: decision.approvalRequest.id,
1712
+ capabilities: decision.capabilities,
1713
+ },
1714
+ });
1715
+ }
1716
+ }
1717
+
1718
+ private async executeNode(
1719
+ node: WorkflowNode,
1720
+ workflow: Workflow,
1721
+ run: WorkflowRun,
1722
+ outputs: Map<string, string>,
1723
+ started: Set<string>,
1724
+ completed: Set<string>,
1725
+ ): Promise<void> {
1726
+ started.add(node.id);
1727
+ const nodeRun = run.nodeRuns.find((candidate) => candidate.nodeId === node.id) as WorkflowNodeRun;
1728
+ const enabledAdapters = readEnabledAdapters(run.metadata);
1729
+ if (enabledAdapters.length > 0 && !enabledAdapters.includes(node.adapterId)) {
1730
+ nodeRun.status = SKIPPED;
1731
+ nodeRun.finishedAt = Date.now();
1732
+ completed.add(node.id);
1733
+ workflowStore.setRun(run);
1734
+ return;
1735
+ }
1736
+ if (this.isCanceling(run.id)) {
1737
+ nodeRun.status = 'canceled';
1738
+ nodeRun.finishedAt = Date.now();
1739
+ workflowStore.setRun(run);
1740
+ throw new WorkflowCanceledError();
1741
+ }
1742
+
1743
+ nodeRun.status = 'running';
1744
+ nodeRun.startedAt = Date.now();
1745
+ nodeRun.permissionMode = resolveNodePermissionMode(node, resolveWorkflowWorkspace(run.metadata));
1746
+ workflowStore.setRun(run);
1747
+
1748
+ const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
1749
+ const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
1750
+ const contextPacket = buildWorkflowContextPacket({
1751
+ run,
1752
+ node,
1753
+ workspaceTarget,
1754
+ inputContext,
1755
+ inputNodeIds: node.inputs,
1756
+ });
1757
+ nodeRun.contextPacket = contextPacket;
1758
+ workflowStore.setRun(run);
1759
+ const prompt = [
1760
+ 'Original user request (primary task; answer this directly even if the workspace is empty):',
1761
+ run.input?.trim() || '(No original user request was provided.)',
1762
+ formatContextPacketForPrompt(contextPacket),
1763
+ inputContext
1764
+ ? `Upstream workflow context from prior agents:\n${inputContext}`
1765
+ : '',
1766
+ `Current workflow step instructions:\n${node.prompt}`,
1767
+ workspaceContextPrompt(workspaceTarget),
1768
+ ].filter(Boolean).join('\n\n');
1769
+ const settings = getMetadataRecord(run.metadata, 'settings');
1770
+ const projectPath = workspaceTarget.projectPath;
1771
+ const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
1772
+ const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
1773
+ const baseRef = readString(settings.baseRef) ?? 'HEAD';
1774
+ const effectivePermissionMode = resolveNodePermissionMode(node, workspaceTarget);
1775
+ const effectiveModel = await resolveWorkflowModel(node.adapterId, node.model);
1776
+ if (effectiveModel !== node.model) {
1777
+ nodeRun.model = effectiveModel;
1778
+ const modelFallbackEvents = Array.isArray(run.metadata?.modelFallbackEvents)
1779
+ ? run.metadata.modelFallbackEvents
1780
+ : [];
1781
+ run.metadata = {
1782
+ ...run.metadata,
1783
+ modelFallbackEvents: [
1784
+ ...modelFallbackEvents,
1785
+ {
1786
+ nodeId: node.id,
1787
+ adapterId: node.adapterId,
1788
+ requestedModel: node.model,
1789
+ effectiveModel,
1790
+ changedAt: Date.now(),
1791
+ },
1792
+ ],
1793
+ };
1794
+ workflowStore.setRun(run);
1795
+ }
1796
+ const permissionPolicy = permissionPolicyFromRun(run);
1797
+ nodeRun.permissionPolicy = permissionPolicy;
1798
+ const permissionDecision = evaluatePermissionRequest({
1799
+ policy: permissionPolicy,
1800
+ request: {
1801
+ source: 'workflow_node',
1802
+ toolName: node.adapterId,
1803
+ input: {
1804
+ assignment: node.assignment,
1805
+ stage: node.stage,
1806
+ toolsSettings: node.toolsSettings,
1807
+ },
1808
+ cwd: projectPath,
1809
+ workspacePath: workspaceTarget.appRoot,
1810
+ targetPaths: [projectPath],
1811
+ summary: [
1812
+ node.agentLabel || node.id,
1813
+ node.stage ? `stage=${node.stage}` : undefined,
1814
+ node.assignment,
1815
+ ].filter(Boolean).join(' / '),
1816
+ },
1817
+ context: {
1818
+ runId: run.id,
1819
+ nodeId: node.id,
1820
+ workflowId: run.workflowId,
1821
+ adapterId: node.adapterId,
1822
+ agentLabel: node.agentLabel,
1823
+ userId: readNotificationUserId(run.metadata),
1824
+ },
1825
+ });
1826
+ this.recordPermissionDecision(run, nodeRun, permissionDecision);
1827
+ workflowStore.setRun(run);
1828
+ if (permissionDecision.behavior === 'deny') {
1829
+ nodeRun.finishedAt = Date.now();
1830
+ nodeRun.status = 'failed';
1831
+ nodeRun.error = permissionDecision.message;
1832
+ workflowStore.setRun(run);
1833
+ if (node.onFail === 'continue') {
1834
+ completed.add(node.id);
1835
+ return;
1836
+ }
1837
+ throw new Error(permissionDecision.message);
1838
+ }
1839
+ let body: { id?: string; error?: { message?: string } };
1840
+ try {
1841
+ const submit = await fetch(`${localHermesBaseUrl()}/tasks`, {
1842
+ method: 'POST',
1843
+ headers: { 'content-type': 'application/json' },
1844
+ body: JSON.stringify({
1845
+ adapterId: node.adapterId,
1846
+ contextId: run.contextId,
1847
+ message: {
1848
+ messageId: newId('msg'),
1849
+ role: 'user',
1850
+ parts: [{ kind: 'text', text: prompt }],
1851
+ },
1852
+ metadata: {
1853
+ workflowRunId: run.id,
1854
+ workflowNodeId: node.id,
1855
+ agentInstanceId: node.agentInstanceId,
1856
+ agentLabel: node.agentLabel,
1857
+ assignment: node.assignment,
1858
+ model: effectiveModel,
1859
+ permissionMode: effectivePermissionMode,
1860
+ permissionPolicy,
1861
+ permissionPolicyContext: {
1862
+ runId: run.id,
1863
+ nodeId: node.id,
1864
+ workflowId: run.workflowId,
1865
+ adapterId: node.adapterId,
1866
+ agentLabel: node.agentLabel,
1867
+ userId: readNotificationUserId(run.metadata),
1868
+ },
1869
+ toolsSettings: node.toolsSettings,
1870
+ projectPath,
1871
+ workspaceTarget: workspaceTargetMetadata(workspaceTarget),
1872
+ workspace: {
1873
+ kind: isolation,
1874
+ projectPath,
1875
+ baseRef,
1876
+ keepAfterCompletion,
1877
+ },
1878
+ },
1879
+ }),
1880
+ });
1881
+ body = await submit.json() as { id?: string; error?: { message?: string } };
1882
+ if (!submit.ok || !body.id) {
1883
+ throw new Error(body.error?.message ?? `Workflow node ${node.id} submit failed.`);
1884
+ }
1885
+ } catch (error) {
1886
+ nodeRun.finishedAt = Date.now();
1887
+ nodeRun.status = 'failed';
1888
+ nodeRun.error = error instanceof Error ? error.message : String(error);
1889
+ workflowStore.setRun(run);
1890
+ if (isExternalDirectoryPermissionError(nodeRun.error)) {
1891
+ completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1892
+ workflowStore.setRun(run);
1893
+ return;
1894
+ }
1895
+ if (await this.runFallbackAfterFailure(
1896
+ node,
1897
+ workflow,
1898
+ run,
1899
+ outputs,
1900
+ started,
1901
+ completed,
1902
+ nodeRun.error,
1903
+ 'provider_failure',
1904
+ )) {
1905
+ return;
1906
+ }
1907
+ if (node.onFail === 'continue') {
1908
+ completed.add(node.id);
1909
+ return;
1910
+ }
1911
+ throw error;
1912
+ }
1913
+ nodeRun.hermesTaskId = body.id;
1914
+ workflowStore.setRun(run);
1915
+
1916
+ if (this.isCanceling(run.id)) {
1917
+ await cancelHermesTask(body.id);
1918
+ nodeRun.status = 'canceled';
1919
+ nodeRun.finishedAt = Date.now();
1920
+ workflowStore.setRun(run);
1921
+ throw new WorkflowCanceledError();
1922
+ }
1923
+
1924
+ let result: TaskResult;
1925
+ try {
1926
+ result = await waitForTask(
1927
+ body.id,
1928
+ () => this.isCanceling(run.id),
1929
+ (snapshot) => {
1930
+ nodeRun.outputText = snapshot.text || nodeRun.outputText;
1931
+ nodeRun.messages = snapshot.messages;
1932
+ nodeRun.artifacts = snapshot.artifacts;
1933
+ nodeRun.error = snapshot.error;
1934
+ workflowStore.setRun(run);
1935
+ },
1936
+ node.timeoutMs,
1937
+ );
1938
+ } catch (error) {
1939
+ if (!(error instanceof WorkflowNodeTimeoutError)) {
1940
+ throw error;
1941
+ }
1942
+
1943
+ await cancelHermesTask(body.id);
1944
+ nodeRun.finishedAt = Date.now();
1945
+ nodeRun.status = 'failed';
1946
+ nodeRun.error = error.message;
1947
+ if (nodeRun.outputText) {
1948
+ outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1949
+ }
1950
+ workflowStore.setRun(run);
1951
+ if (isExternalDirectoryPermissionError(nodeRun.error)) {
1952
+ completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1953
+ workflowStore.setRun(run);
1954
+ return;
1955
+ }
1956
+ if (await this.runFallbackAfterFailure(
1957
+ node,
1958
+ workflow,
1959
+ run,
1960
+ outputs,
1961
+ started,
1962
+ completed,
1963
+ nodeRun.error,
1964
+ 'timeout',
1965
+ )) {
1966
+ return;
1967
+ }
1968
+ if (node.onFail === 'continue') {
1969
+ completed.add(node.id);
1970
+ return;
1971
+ }
1972
+ throw error;
1973
+ }
1974
+ nodeRun.finishedAt = Date.now();
1975
+ nodeRun.outputText = result.text;
1976
+ nodeRun.messages = result.messages;
1977
+ nodeRun.artifacts = result.artifacts;
1978
+ if (this.isCanceling(run.id)) {
1979
+ nodeRun.status = 'canceled';
1980
+ workflowStore.setRun(run);
1981
+ throw new WorkflowCanceledError();
1982
+ }
1983
+ if (result.state === 'completed') {
1984
+ let outputForContext = result.text;
1985
+ if (requiresHandoffArtifact(node)) {
1986
+ const handoffParse = parseHandoffArtifact(handoffArtifactSource(result), {
1987
+ workflowRunId: run.id,
1988
+ nodeId: node.id,
1989
+ agentLabel: node.agentLabel,
1990
+ stage: node.stage,
1991
+ });
1992
+ if (!handoffParse.ok) {
1993
+ const visibleHandoffError = handoffParse.error.startsWith('Invalid handoff artifact')
1994
+ ? handoffParse.error
1995
+ : `Invalid handoff artifact: ${handoffParse.error}`;
1996
+ nodeRun.status = 'failed';
1997
+ nodeRun.error = visibleHandoffError;
1998
+ workflowStore.setRun(run);
1999
+ if (await this.runFallbackAfterFailure(
2000
+ node,
2001
+ workflow,
2002
+ run,
2003
+ outputs,
2004
+ started,
2005
+ completed,
2006
+ visibleHandoffError,
2007
+ 'invalid_output',
2008
+ )) {
2009
+ return;
2010
+ }
2011
+ if (node.onFail === 'continue') {
2012
+ completed.add(node.id);
2013
+ return;
2014
+ }
2015
+ throw new Error(visibleHandoffError);
2016
+ }
2017
+
2018
+ nodeRun.handoffArtifact = handoffParse.artifact;
2019
+ nodeRun.artifacts = [
2020
+ ...(nodeRun.artifacts ?? []).filter((artifact) => artifact.type !== 'handoff-artifact'),
2021
+ handoffArtifactToWorkflowArtifact(handoffParse.artifact),
2022
+ ];
2023
+ outputForContext = formatHandoffArtifactForContext(handoffParse.artifact);
2024
+ }
2025
+
2026
+ outputs.set(node.id, compactOutputForContext(outputForContext));
2027
+ completed.add(node.id);
2028
+ nodeRun.status = 'completed';
2029
+ workflowStore.setRun(run);
2030
+ this.maybeAddRepairCycle(node, workflow, run, result);
2031
+ return;
2032
+ }
2033
+ if (result.state === 'canceled') {
2034
+ nodeRun.status = 'canceled';
2035
+ workflowStore.setRun(run);
2036
+ throw new WorkflowCanceledError();
2037
+ }
2038
+
2039
+ nodeRun.status = 'failed';
2040
+ nodeRun.error = result.error ?? `Hermes task ended with ${result.state}`;
2041
+ workflowStore.setRun(run);
2042
+ if (isExternalDirectoryPermissionError(`${nodeRun.error}\n${nodeRun.outputText ?? ''}`)) {
2043
+ completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
2044
+ workflowStore.setRun(run);
2045
+ return;
2046
+ }
2047
+ if (await this.runFallbackAfterFailure(
2048
+ node,
2049
+ workflow,
2050
+ run,
2051
+ outputs,
2052
+ started,
2053
+ completed,
2054
+ nodeRun.error,
2055
+ classifyWorkflowFailure(`${nodeRun.error}\n${nodeRun.outputText ?? ''}`),
2056
+ )) {
2057
+ return;
2058
+ }
2059
+ if (node.onFail === 'continue') {
2060
+ if (nodeRun.outputText) {
2061
+ outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
2062
+ }
2063
+ completed.add(node.id);
2064
+ return;
2065
+ }
2066
+ throw new Error(nodeRun.error);
2067
+ }
2068
+ }
2069
+
2070
+ export const workflowRunner = new WorkflowRunner();