funolio-agent 1.0.53 → 1.1.65

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 (276) hide show
  1. package/dist/approval.d.ts +1 -6
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +2 -7
  4. package/dist/approval.js.map +1 -1
  5. package/dist/auth/credential-reader.d.ts.map +1 -1
  6. package/dist/auth/credential-reader.js +4 -3
  7. package/dist/auth/credential-reader.js.map +1 -1
  8. package/dist/auth/token-refresh.d.ts +8 -0
  9. package/dist/auth/token-refresh.d.ts.map +1 -1
  10. package/dist/auth/token-refresh.js +82 -52
  11. package/dist/auth/token-refresh.js.map +1 -1
  12. package/dist/auto-organizer.d.ts.map +1 -1
  13. package/dist/auto-organizer.js +6 -7
  14. package/dist/auto-organizer.js.map +1 -1
  15. package/dist/bench-prefix.d.ts +16 -0
  16. package/dist/bench-prefix.d.ts.map +1 -0
  17. package/dist/bench-prefix.js +25 -0
  18. package/dist/bench-prefix.js.map +1 -0
  19. package/dist/bot-manager.d.ts +5 -1
  20. package/dist/bot-manager.d.ts.map +1 -1
  21. package/dist/bot-manager.js +46 -27
  22. package/dist/bot-manager.js.map +1 -1
  23. package/dist/chat-sync.d.ts +42 -0
  24. package/dist/chat-sync.d.ts.map +1 -0
  25. package/dist/chat-sync.js +95 -0
  26. package/dist/chat-sync.js.map +1 -0
  27. package/dist/clerk-model.d.ts +7 -0
  28. package/dist/clerk-model.d.ts.map +1 -1
  29. package/dist/clerk-model.js +42 -8
  30. package/dist/clerk-model.js.map +1 -1
  31. package/dist/cli-bootstrap-history.d.ts +10 -0
  32. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  33. package/dist/cli-bootstrap-history.js +112 -0
  34. package/dist/cli-bootstrap-history.js.map +1 -0
  35. package/dist/cli-models.d.ts +8 -0
  36. package/dist/cli-models.d.ts.map +1 -0
  37. package/dist/cli-models.js +91 -0
  38. package/dist/cli-models.js.map +1 -0
  39. package/dist/cli-session-epoch.d.ts +13 -3
  40. package/dist/cli-session-epoch.d.ts.map +1 -1
  41. package/dist/cli-session-epoch.js +53 -4
  42. package/dist/cli-session-epoch.js.map +1 -1
  43. package/dist/cli-session-registry.d.ts +35 -0
  44. package/dist/cli-session-registry.d.ts.map +1 -0
  45. package/dist/cli-session-registry.js +177 -0
  46. package/dist/cli-session-registry.js.map +1 -0
  47. package/dist/cli.js +62 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/codex-app-server-manager.d.ts +189 -0
  50. package/dist/codex-app-server-manager.d.ts.map +1 -0
  51. package/dist/codex-app-server-manager.js +1468 -0
  52. package/dist/codex-app-server-manager.js.map +1 -0
  53. package/dist/commands/init.d.ts.map +1 -1
  54. package/dist/commands/init.js +8 -30
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/pool.d.ts +32 -0
  57. package/dist/commands/pool.d.ts.map +1 -1
  58. package/dist/commands/pool.js +145 -66
  59. package/dist/commands/pool.js.map +1 -1
  60. package/dist/commands/setup.d.ts +4 -1
  61. package/dist/commands/setup.d.ts.map +1 -1
  62. package/dist/commands/setup.js +9 -25
  63. package/dist/commands/setup.js.map +1 -1
  64. package/dist/commands/start.d.ts +21 -0
  65. package/dist/commands/start.d.ts.map +1 -1
  66. package/dist/commands/start.js +559 -63
  67. package/dist/commands/start.js.map +1 -1
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +5 -2
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/completion-marker.d.ts +7 -0
  72. package/dist/completion-marker.d.ts.map +1 -0
  73. package/dist/completion-marker.js +28 -0
  74. package/dist/completion-marker.js.map +1 -0
  75. package/dist/config.d.ts +7 -2
  76. package/dist/config.d.ts.map +1 -1
  77. package/dist/config.js +184 -60
  78. package/dist/config.js.map +1 -1
  79. package/dist/context-window.d.ts +37 -1
  80. package/dist/context-window.d.ts.map +1 -1
  81. package/dist/context-window.js +210 -17
  82. package/dist/context-window.js.map +1 -1
  83. package/dist/live-activity.d.ts +31 -0
  84. package/dist/live-activity.d.ts.map +1 -0
  85. package/dist/live-activity.js +36 -0
  86. package/dist/live-activity.js.map +1 -0
  87. package/dist/local-chat-execution.d.ts +114 -0
  88. package/dist/local-chat-execution.d.ts.map +1 -0
  89. package/dist/local-chat-execution.js +349 -0
  90. package/dist/local-chat-execution.js.map +1 -0
  91. package/dist/local-cli-pty-manager.d.ts +186 -0
  92. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  93. package/dist/local-cli-pty-manager.js +2581 -164
  94. package/dist/local-cli-pty-manager.js.map +1 -1
  95. package/dist/local-conversation-gateway.d.ts +110 -0
  96. package/dist/local-conversation-gateway.d.ts.map +1 -0
  97. package/dist/local-conversation-gateway.js +175 -0
  98. package/dist/local-conversation-gateway.js.map +1 -0
  99. package/dist/local-data.d.ts +276 -5
  100. package/dist/local-data.d.ts.map +1 -1
  101. package/dist/local-data.js +1201 -86
  102. package/dist/local-data.js.map +1 -1
  103. package/dist/local-db.d.ts +6 -0
  104. package/dist/local-db.d.ts.map +1 -1
  105. package/dist/local-db.js +428 -2
  106. package/dist/local-db.js.map +1 -1
  107. package/dist/local-funnel.d.ts.map +1 -1
  108. package/dist/local-funnel.js +6 -5
  109. package/dist/local-funnel.js.map +1 -1
  110. package/dist/local-server.d.ts +55 -0
  111. package/dist/local-server.d.ts.map +1 -1
  112. package/dist/local-server.js +3281 -441
  113. package/dist/local-server.js.map +1 -1
  114. package/dist/managed-process-registry.d.ts +59 -0
  115. package/dist/managed-process-registry.d.ts.map +1 -0
  116. package/dist/managed-process-registry.js +390 -0
  117. package/dist/managed-process-registry.js.map +1 -0
  118. package/dist/mcp/claude-config-writer.d.ts +5 -5
  119. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  120. package/dist/mcp/claude-config-writer.js +19 -11
  121. package/dist/mcp/claude-config-writer.js.map +1 -1
  122. package/dist/mcp/index.d.ts +4 -2
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js.map +1 -1
  125. package/dist/mcp/sync-cli-config.d.ts +42 -4
  126. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  127. package/dist/mcp/sync-cli-config.js +497 -17
  128. package/dist/mcp/sync-cli-config.js.map +1 -1
  129. package/dist/message-loop.d.ts +6 -0
  130. package/dist/message-loop.d.ts.map +1 -1
  131. package/dist/message-loop.js +281 -89
  132. package/dist/message-loop.js.map +1 -1
  133. package/dist/mqtt-client.d.ts +44 -1
  134. package/dist/mqtt-client.d.ts.map +1 -1
  135. package/dist/mqtt-client.js +284 -46
  136. package/dist/mqtt-client.js.map +1 -1
  137. package/dist/mqtt-data-relay.d.ts +44 -0
  138. package/dist/mqtt-data-relay.d.ts.map +1 -0
  139. package/dist/mqtt-data-relay.js +106 -0
  140. package/dist/mqtt-data-relay.js.map +1 -0
  141. package/dist/oauth.d.ts.map +1 -1
  142. package/dist/oauth.js +69 -29
  143. package/dist/oauth.js.map +1 -1
  144. package/dist/orchestration/capabilities.d.ts +13 -0
  145. package/dist/orchestration/capabilities.d.ts.map +1 -0
  146. package/dist/orchestration/capabilities.js +152 -0
  147. package/dist/orchestration/capabilities.js.map +1 -0
  148. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  149. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  150. package/dist/orchestration/dispatch-executor.js +266 -0
  151. package/dist/orchestration/dispatch-executor.js.map +1 -0
  152. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  153. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  154. package/dist/orchestration/dispatch-hint.js +247 -0
  155. package/dist/orchestration/dispatch-hint.js.map +1 -0
  156. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  157. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  158. package/dist/orchestration/dispatch-runner.js +604 -0
  159. package/dist/orchestration/dispatch-runner.js.map +1 -0
  160. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  161. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  162. package/dist/orchestration/dispatch-tools.js +328 -0
  163. package/dist/orchestration/dispatch-tools.js.map +1 -0
  164. package/dist/orchestration/front-door-policy.d.ts +35 -10
  165. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  166. package/dist/orchestration/front-door-policy.js +30 -267
  167. package/dist/orchestration/front-door-policy.js.map +1 -1
  168. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  169. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  170. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  171. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  172. package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
  173. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  174. package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
  175. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  176. package/dist/orchestration/plan-import.d.ts +39 -0
  177. package/dist/orchestration/plan-import.d.ts.map +1 -0
  178. package/dist/orchestration/plan-import.js +547 -0
  179. package/dist/orchestration/plan-import.js.map +1 -0
  180. package/dist/orchestration/validation.d.ts +40 -0
  181. package/dist/orchestration/validation.d.ts.map +1 -0
  182. package/dist/orchestration/validation.js +203 -0
  183. package/dist/orchestration/validation.js.map +1 -0
  184. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  185. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  186. package/dist/orchestration/worker-operating-prompt.js +36 -46
  187. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  188. package/dist/orchestrator.d.ts +214 -33
  189. package/dist/orchestrator.d.ts.map +1 -1
  190. package/dist/orchestrator.js +2200 -1100
  191. package/dist/orchestrator.js.map +1 -1
  192. package/dist/providers/anthropic.d.ts.map +1 -1
  193. package/dist/providers/anthropic.js +8 -4
  194. package/dist/providers/anthropic.js.map +1 -1
  195. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  196. package/dist/providers/claude-cli-prompt.js +49 -5
  197. package/dist/providers/claude-cli-prompt.js.map +1 -1
  198. package/dist/providers/claude-cli.d.ts.map +1 -1
  199. package/dist/providers/claude-cli.js +81 -5
  200. package/dist/providers/claude-cli.js.map +1 -1
  201. package/dist/providers/codex-cli.d.ts +10 -6
  202. package/dist/providers/codex-cli.d.ts.map +1 -1
  203. package/dist/providers/codex-cli.js +204 -26
  204. package/dist/providers/codex-cli.js.map +1 -1
  205. package/dist/providers/google.d.ts.map +1 -1
  206. package/dist/providers/google.js +15 -5
  207. package/dist/providers/google.js.map +1 -1
  208. package/dist/providers/index.d.ts +15 -1
  209. package/dist/providers/index.d.ts.map +1 -1
  210. package/dist/providers/index.js.map +1 -1
  211. package/dist/providers/openai.d.ts +1 -1
  212. package/dist/providers/openai.d.ts.map +1 -1
  213. package/dist/providers/openai.js +13 -5
  214. package/dist/providers/openai.js.map +1 -1
  215. package/dist/response-guard.js +1 -1
  216. package/dist/response-guard.js.map +1 -1
  217. package/dist/server-adapter.d.ts +8 -0
  218. package/dist/server-adapter.d.ts.map +1 -1
  219. package/dist/server-adapter.js +7 -0
  220. package/dist/server-adapter.js.map +1 -1
  221. package/dist/service-mode.d.ts +1 -1
  222. package/dist/service-mode.d.ts.map +1 -1
  223. package/dist/service-mode.js +64 -1
  224. package/dist/service-mode.js.map +1 -1
  225. package/dist/service-setup-only.d.ts +8 -0
  226. package/dist/service-setup-only.d.ts.map +1 -0
  227. package/dist/service-setup-only.js +37 -0
  228. package/dist/service-setup-only.js.map +1 -0
  229. package/dist/slash-commands.d.ts +21 -0
  230. package/dist/slash-commands.d.ts.map +1 -0
  231. package/dist/slash-commands.js +99 -0
  232. package/dist/slash-commands.js.map +1 -0
  233. package/dist/subagent/index.d.ts +4 -2
  234. package/dist/subagent/index.d.ts.map +1 -1
  235. package/dist/subagent/index.js.map +1 -1
  236. package/dist/summarization-pipeline.d.ts.map +1 -1
  237. package/dist/summarization-pipeline.js +1 -9
  238. package/dist/summarization-pipeline.js.map +1 -1
  239. package/dist/token-counter.d.ts.map +1 -1
  240. package/dist/token-counter.js +11 -4
  241. package/dist/token-counter.js.map +1 -1
  242. package/dist/tool-filter.d.ts.map +1 -1
  243. package/dist/tool-filter.js +10 -6
  244. package/dist/tool-filter.js.map +1 -1
  245. package/dist/tools/admin-tools.d.ts.map +1 -1
  246. package/dist/tools/admin-tools.js +20 -5
  247. package/dist/tools/admin-tools.js.map +1 -1
  248. package/dist/tools/index.d.ts.map +1 -1
  249. package/dist/tools/index.js +2 -1
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/run-command.d.ts.map +1 -1
  252. package/dist/tools/run-command.js +5 -1
  253. package/dist/tools/run-command.js.map +1 -1
  254. package/dist/tools/search-conversation-history.d.ts +16 -0
  255. package/dist/tools/search-conversation-history.d.ts.map +1 -0
  256. package/dist/tools/search-conversation-history.js +334 -0
  257. package/dist/tools/search-conversation-history.js.map +1 -0
  258. package/dist/tools/todo-tasks.d.ts.map +1 -1
  259. package/dist/tools/todo-tasks.js +77 -5
  260. package/dist/tools/todo-tasks.js.map +1 -1
  261. package/dist/usage-log.d.ts +62 -0
  262. package/dist/usage-log.d.ts.map +1 -0
  263. package/dist/usage-log.js +98 -0
  264. package/dist/usage-log.js.map +1 -0
  265. package/dist/wizard-state.d.ts +20 -0
  266. package/dist/wizard-state.d.ts.map +1 -1
  267. package/dist/wizard-state.js +90 -3
  268. package/dist/wizard-state.js.map +1 -1
  269. package/dist/wizard-support.d.ts.map +1 -1
  270. package/dist/wizard-support.js +27 -1
  271. package/dist/wizard-support.js.map +1 -1
  272. package/dist/workflow-engine.d.ts +44 -2
  273. package/dist/workflow-engine.d.ts.map +1 -1
  274. package/dist/workflow-engine.js +932 -111
  275. package/dist/workflow-engine.js.map +1 -1
  276. package/package.json +2 -2
@@ -47,25 +47,37 @@ var __importStar = (this && this.__importStar) || (function () {
47
47
  })();
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.OrchestratorAgent = void 0;
50
+ exports.buildLocalDesktopOrchestratorRuntime = buildLocalDesktopOrchestratorRuntime;
51
+ exports.buildNarrationStreamGate = buildNarrationStreamGate;
52
+ const index_1 = require("./providers/index");
53
+ const approval_1 = require("./approval");
50
54
  const status_parser_1 = require("./orchestration/status-parser");
55
+ const validation_1 = require("./orchestration/validation");
51
56
  const front_door_policy_1 = require("./orchestration/front-door-policy");
52
57
  const deterministic_path_1 = require("./orchestration/deterministic-path");
53
58
  const orchestrator_operating_prompt_1 = require("./orchestration/orchestrator-operating-prompt");
54
59
  const policy_prompt_1 = require("./orchestration/policy-prompt");
60
+ const safeguards_1 = require("./orchestration/safeguards");
55
61
  const orchestrator_blocked_prompt_1 = require("./orchestration/orchestrator-blocked-prompt");
56
62
  const orchestrator_final_response_prompt_1 = require("./orchestration/orchestrator-final-response-prompt");
57
63
  const worker_operating_prompt_1 = require("./orchestration/worker-operating-prompt");
64
+ const capabilities_1 = require("./orchestration/capabilities");
58
65
  const policy_detection_1 = require("./policy-detection");
59
66
  const execution_contract_1 = require("./execution-contract");
60
67
  const state_1 = require("./orchestration/state");
61
68
  const orchestrator_profile_1 = require("./orchestrator-profile");
62
69
  const context_window_1 = require("./context-window");
70
+ const cli_bootstrap_history_1 = require("./cli-bootstrap-history");
71
+ const plan_import_1 = require("./orchestration/plan-import");
72
+ const index_2 = require("./index");
63
73
  const data = __importStar(require("./local-data"));
74
+ const cli_session_epoch_1 = require("./cli-session-epoch");
64
75
  const storage_mode_1 = require("./storage-mode");
76
+ const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
77
+ const codex_app_server_manager_1 = require("./codex-app-server-manager");
65
78
  const fs = __importStar(require("fs"));
66
79
  const os = __importStar(require("os"));
67
80
  const path = __importStar(require("path"));
68
- const ORCHESTRATOR_ROLE_SETTING_KEY = 'orchestrator_role_assignments';
69
81
  const ORCHESTRATOR_WORKFLOW_SETTING_KEY = 'orchestrator_preferred_workflow';
70
82
  const ORCHESTRATOR_TEMPLATE_SETTING_KEY = 'orchestrator_default_workflow_template_id';
71
83
  const HEARTBEAT_MS = 30_000;
@@ -80,13 +92,283 @@ const ORCHESTRATION_NODE_RETRY_LIMITS = {
80
92
  finalize_response: 0,
81
93
  require_confirmation: 0,
82
94
  };
95
+ function isAbortLikeError(err) {
96
+ return err?.name === 'AbortError'
97
+ || err?.code === 'ABORT_ERR'
98
+ || /\baborted\b/i.test(String(err?.message || err || ''));
99
+ }
83
100
  // ─── Orchestrator Agent ──────────────────────────────────────────
101
+ function resolveLocalProviderCredentials(providerName, profile, explicitConnection) {
102
+ if (index_1.CLI_PROVIDERS.has(providerName)) {
103
+ return { apiKey: 'cli-auth' };
104
+ }
105
+ if (explicitConnection?.oauth_token) {
106
+ return { apiKey: explicitConnection.oauth_token, authMode: 'oauth-bearer' };
107
+ }
108
+ if (explicitConnection?.api_key_enc) {
109
+ return { apiKey: explicitConnection.api_key_enc };
110
+ }
111
+ const directConnection = profile?.provider_connection_id
112
+ ? data.getProviderConnection(profile.provider_connection_id)
113
+ : undefined;
114
+ if (directConnection?.oauth_token) {
115
+ return { apiKey: directConnection.oauth_token, authMode: 'oauth-bearer' };
116
+ }
117
+ if (directConnection?.api_key_enc) {
118
+ return { apiKey: directConnection.api_key_enc };
119
+ }
120
+ const providerConnection = data.findProviderConnection(providerName);
121
+ if (providerConnection?.oauth_token) {
122
+ return { apiKey: providerConnection.oauth_token, authMode: 'oauth-bearer' };
123
+ }
124
+ if (providerConnection?.api_key_enc) {
125
+ return { apiKey: providerConnection.api_key_enc };
126
+ }
127
+ if (profile?.api_key_enc) {
128
+ return { apiKey: profile.api_key_enc };
129
+ }
130
+ switch (providerName) {
131
+ case 'anthropic':
132
+ if (process.env.ANTHROPIC_API_KEY)
133
+ return { apiKey: process.env.ANTHROPIC_API_KEY };
134
+ break;
135
+ case 'openai':
136
+ if (process.env.OPENAI_API_KEY)
137
+ return { apiKey: process.env.OPENAI_API_KEY };
138
+ break;
139
+ case 'google':
140
+ if (process.env.GOOGLE_API_KEY)
141
+ return { apiKey: process.env.GOOGLE_API_KEY };
142
+ break;
143
+ }
144
+ throw new Error(`No credentials configured for provider ${providerName}.`);
145
+ }
146
+ function buildLocalDesktopOrchestratorRuntime(selectedBot) {
147
+ if (data.isClerkOrchestratorEnabled()) {
148
+ const clerkConfig = data.getResolvedClerkConfigInfo();
149
+ const clerkConnection = clerkConfig.providerConnectionId
150
+ ? data.getProviderConnection(clerkConfig.providerConnectionId)
151
+ : undefined;
152
+ const providerName = (clerkConfig.provider || '').trim();
153
+ const model = (clerkConfig.model || '').trim() || 'default';
154
+ if (!providerName) {
155
+ throw new Error('Clerk is selected as orchestrator, but no clerk provider is configured.');
156
+ }
157
+ const credentials = resolveLocalProviderCredentials(providerName, null, clerkConnection);
158
+ const llm = (0, index_1.createProvider)(providerName, {
159
+ apiKey: credentials.apiKey,
160
+ model,
161
+ runtimeMode: 'local_desktop',
162
+ ...(credentials.authMode ? { authMode: credentials.authMode } : {}),
163
+ });
164
+ return {
165
+ kind: 'clerk',
166
+ llm,
167
+ providerName,
168
+ model,
169
+ agentName: 'Orchestrator',
170
+ botId: null,
171
+ modelLabel: model || null,
172
+ };
173
+ }
174
+ // Orchestrator must be designated explicitly (is_orchestrator=1 or selectedBot).
175
+ // No fallback to the default worker bot — that's what created the "Ben is both
176
+ // orchestrator and worker" bug. If neither Clerk nor an orchestrator bot is
177
+ // configured, the runtime should refuse to start rather than silently reuse
178
+ // whichever bot happens to be is_default=1.
179
+ const profile = selectedBot || data.getOrchestratorBot();
180
+ if (!profile) {
181
+ throw new Error('No orchestrator bot is configured. Mark a bot with is_orchestrator=1 or enable Clerk orchestration.');
182
+ }
183
+ return buildLocalDesktopRuntimeFromProfile(profile);
184
+ }
185
+ function buildLocalDesktopRuntimeFromProfile(profile) {
186
+ const credentials = resolveLocalProviderCredentials(profile.provider, profile);
187
+ const llm = (0, index_1.createProvider)(profile.provider, {
188
+ apiKey: credentials.apiKey,
189
+ model: (profile.model || '').trim() || 'default',
190
+ runtimeMode: 'local_desktop',
191
+ ...(credentials.authMode ? { authMode: credentials.authMode } : {}),
192
+ });
193
+ return {
194
+ kind: 'bot',
195
+ llm,
196
+ providerName: profile.provider,
197
+ model: profile.model || null,
198
+ agentName: profile.name,
199
+ botId: profile.id,
200
+ modelLabel: profile.model || profile.name,
201
+ };
202
+ }
203
+ function scorePlannerModel(profile) {
204
+ const model = String(profile.model || '').trim().toLowerCase();
205
+ const provider = String(profile.provider || '').trim().toLowerCase();
206
+ let score = 50;
207
+ if (/gpt-5\.4/.test(model))
208
+ score += 60;
209
+ else if (/\bopus\b/.test(model))
210
+ score += 58;
211
+ else if (/gpt-5\.2/.test(model))
212
+ score += 54;
213
+ else if (/\bsonnet\b/.test(model))
214
+ score += 48;
215
+ else if (/gpt-5/.test(model))
216
+ score += 46;
217
+ else if (/\bpro\b/.test(model))
218
+ score += 44;
219
+ else if (/\bcodex\b/.test(model))
220
+ score += 38;
221
+ else if (/\bflash\b/.test(model))
222
+ score += 28;
223
+ else if (/\bmini\b/.test(model))
224
+ score += 24;
225
+ else if (/\bhaiku\b/.test(model))
226
+ score += 22;
227
+ else if (model)
228
+ score += 30;
229
+ if (provider === 'openai')
230
+ score += 8;
231
+ else if (provider === 'anthropic' || provider === 'claude-cli')
232
+ score += 7;
233
+ else if (provider === 'google')
234
+ score += 6;
235
+ else if (provider === 'codex-cli')
236
+ score += 5;
237
+ return score;
238
+ }
239
+ function selectStrongestPlannerBot(bots) {
240
+ if (bots.length === 0)
241
+ return null;
242
+ return [...bots]
243
+ .sort((a, b) => {
244
+ const scoreDiff = scorePlannerModel(b) - scorePlannerModel(a);
245
+ if (scoreDiff !== 0)
246
+ return scoreDiff;
247
+ return a.name.localeCompare(b.name);
248
+ })[0] || null;
249
+ }
250
+ /**
251
+ * Builds a chunk-forwarding gate used to stream orchestrator narration to
252
+ * the user while preserving the structured-decision contract:
253
+ *
254
+ * - Forwards pre-sentinel text to onNarrationChunk
255
+ * - Stops as soon as ===DECISION=== sentinel is seen (the rest is JSON)
256
+ * - Detects "looks like raw JSON, no sentinel coming" early and suppresses
257
+ * forwarding entirely so JSON does not leak to the user
258
+ * - Holds the first 16 chars to make the narration-vs-JSON decision
259
+ * - Handles sentinel arriving before the sniff length is reached
260
+ *
261
+ * Exported separately so unit tests can exercise the gate logic without
262
+ * standing up a full orchestrator instance.
263
+ */
264
+ function buildNarrationStreamGate(onNarrationChunk) {
265
+ const SNIFF_LEN = 16;
266
+ let buffer = '';
267
+ let suppressed = false;
268
+ let decided = false;
269
+ let forwardedLen = 0;
270
+ const isLikelyRawJsonPrefix = (text) => {
271
+ const trimmed = text.replace(/^\s+/, '');
272
+ if (!trimmed)
273
+ return false;
274
+ if (trimmed.startsWith('{') || trimmed.startsWith('```json') || trimmed.startsWith('```'))
275
+ return true;
276
+ return false;
277
+ };
278
+ return async (chunk) => {
279
+ if (!chunk || suppressed)
280
+ return;
281
+ buffer += chunk;
282
+ const sentinelIdx = buffer.indexOf(orchestrator_operating_prompt_1.ORCHESTRATOR_DECISION_SENTINEL);
283
+ if (sentinelIdx >= 0) {
284
+ const preSentinel = buffer.slice(0, sentinelIdx);
285
+ if (!decided) {
286
+ if (preSentinel.length > 0 && !isLikelyRawJsonPrefix(preSentinel)) {
287
+ try {
288
+ await onNarrationChunk(preSentinel);
289
+ }
290
+ catch { /* swallow */ }
291
+ }
292
+ decided = true;
293
+ }
294
+ else if (sentinelIdx > forwardedLen) {
295
+ const tail = buffer.slice(forwardedLen, sentinelIdx);
296
+ if (tail) {
297
+ try {
298
+ await onNarrationChunk(tail);
299
+ }
300
+ catch { /* swallow */ }
301
+ }
302
+ }
303
+ forwardedLen = sentinelIdx;
304
+ suppressed = true;
305
+ return;
306
+ }
307
+ if (!decided) {
308
+ if (buffer.length < SNIFF_LEN)
309
+ return;
310
+ if (isLikelyRawJsonPrefix(buffer)) {
311
+ suppressed = true;
312
+ return;
313
+ }
314
+ decided = true;
315
+ forwardedLen = buffer.length;
316
+ try {
317
+ await onNarrationChunk(buffer);
318
+ }
319
+ catch { /* swallow */ }
320
+ return;
321
+ }
322
+ const tail = buffer.slice(forwardedLen);
323
+ if (tail) {
324
+ forwardedLen = buffer.length;
325
+ try {
326
+ await onNarrationChunk(tail);
327
+ }
328
+ catch { /* swallow */ }
329
+ }
330
+ };
331
+ }
84
332
  class OrchestratorAgent {
85
- clerk;
333
+ orchestratorRuntime;
86
334
  workflowEngine;
87
335
  lastResponseMeta = null;
88
- constructor(clerk, workflowEngine) {
89
- this.clerk = clerk;
336
+ constructor(orchestratorRuntime, workflowEngine) {
337
+ const runtimeCandidate = orchestratorRuntime;
338
+ if (runtimeCandidate && typeof runtimeCandidate.kind === 'string') {
339
+ this.orchestratorRuntime = orchestratorRuntime;
340
+ }
341
+ else if (runtimeCandidate && typeof runtimeCandidate.respond === 'function') {
342
+ const runtimeInfo = typeof runtimeCandidate.getRuntimeInfo === 'function'
343
+ ? runtimeCandidate.getRuntimeInfo()
344
+ : {};
345
+ this.orchestratorRuntime = {
346
+ kind: 'clerk',
347
+ llm: {
348
+ chat: async (options) => ({
349
+ content: await runtimeCandidate.respond(String(options.messages?.[0]?.content || ''), String(options.system || '')),
350
+ }),
351
+ },
352
+ providerName: runtimeInfo?.provider || 'openai',
353
+ model: runtimeInfo?.model || null,
354
+ agentName: 'Orchestrator',
355
+ botId: null,
356
+ modelLabel: runtimeInfo?.model || null,
357
+ };
358
+ }
359
+ else {
360
+ this.orchestratorRuntime = {
361
+ kind: 'clerk',
362
+ llm: {
363
+ chat: async () => ({ content: '' }),
364
+ },
365
+ providerName: 'openai',
366
+ model: null,
367
+ agentName: 'Orchestrator',
368
+ botId: null,
369
+ modelLabel: null,
370
+ };
371
+ }
90
372
  this.workflowEngine = workflowEngine;
91
373
  }
92
374
  isLocalDesktopRuntime() {
@@ -106,6 +388,8 @@ class OrchestratorAgent {
106
388
  return await work(attempt);
107
389
  }
108
390
  catch (error) {
391
+ if (isAbortLikeError(error))
392
+ throw error;
109
393
  if (attempt >= maxRetries)
110
394
  throw error;
111
395
  attempt += 1;
@@ -132,11 +416,52 @@ class OrchestratorAgent {
132
416
  add('GPT');
133
417
  return Array.from(refs);
134
418
  }
419
+ getOrchestrationRoleLabel(agent) {
420
+ return data.getAgentOrchestrationRoleLabel(agent) || 'general';
421
+ }
422
+ getOrchestrationRoleClass(agent) {
423
+ return String(data.getAgentOrchestrationRoleClass(agent) || '').trim().toLowerCase();
424
+ }
425
+ getOrchestrationRolePriorities(agent) {
426
+ if (!agent)
427
+ return [];
428
+ return data.getAgentRolePriorities(agent).map((role) => role.trim().toLowerCase()).filter(Boolean);
429
+ }
430
+ agentHasAnyRole(agent, roles) {
431
+ if (!agent)
432
+ return false;
433
+ const wanted = new Set(roles.map((role) => role.trim().toLowerCase()).filter(Boolean));
434
+ if (wanted.size === 0)
435
+ return false;
436
+ return this.getOrchestrationRolePriorities(agent).some((role) => wanted.has(role))
437
+ || wanted.has(this.getOrchestrationRoleClass(agent));
438
+ }
439
+ /**
440
+ * The worker's `orchestration_include_user_prompt` setting controls
441
+ * whether their task prompt is appended with the original user
442
+ * request as a reference block. Position-aware (2026-04-19): the
443
+ * FIRST worker in a chain ALWAYS gets the user prompt, regardless
444
+ * of the flag — they have no predecessor handoff to stand in for
445
+ * it, and without the prompt they'd have no concrete context at
446
+ * all. Downstream workers honor the explicit flag. This matches
447
+ * Steven's intent: "turn it off for Ben/John so they just read
448
+ * Brain's handoff, but Brain (the first bot) always gets it."
449
+ * If the user configures a different chain where Ben is first,
450
+ * Ben gets the prompt automatically without the user needing to
451
+ * remember to toggle the flag.
452
+ */
453
+ shouldIncludeOriginalPromptForWorker(agent, isFirstInChain) {
454
+ if (isFirstInChain)
455
+ return true;
456
+ return agent.orchestration_include_user_prompt === 1;
457
+ }
135
458
  describeAgentResponsibilities(agent) {
136
459
  const candidates = [
137
460
  agent.purpose_md,
138
461
  agent.identity_summary,
139
462
  agent.skills_md,
463
+ agent.orchestration_role_label,
464
+ agent.orchestration_role_class,
140
465
  agent.role_label,
141
466
  agent.role_class,
142
467
  ].map((value) => String(value || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
@@ -151,7 +476,7 @@ class OrchestratorAgent {
151
476
  return cleaned.length > 180 ? `${cleaned.slice(0, 177).trim()}...` : cleaned;
152
477
  }
153
478
  }
154
- const normalizedRole = String(agent.role_class || agent.role_label || '').trim().toLowerCase();
479
+ const normalizedRole = this.getOrchestrationRoleClass(agent);
155
480
  if (normalizedRole === 'code' || normalizedRole === 'coding') {
156
481
  return 'implementation, code changes, fixes, and app changes';
157
482
  }
@@ -177,7 +502,7 @@ class OrchestratorAgent {
177
502
  const orchestrationState = (0, state_1.createOrchestrationState)({
178
503
  conversationId: conversationId || null,
179
504
  projectId: conversationProjectId || null,
180
- orchestratorBotId: conversation?.agent_id || null,
505
+ orchestratorBotId: opts.orchestratorBotIdHint || null,
181
506
  userPromptRaw: prompt,
182
507
  });
183
508
  orchestrationState.requestedArtifactTargets = this.extractRequestedArtifactTargets(prompt);
@@ -196,24 +521,36 @@ class OrchestratorAgent {
196
521
  return this.applyPendingPolicy(conversationId, pendingPolicy);
197
522
  }
198
523
  const promptAssignments = this.parseRoleAssignments(prompt);
199
- const selectedOrchestrator = this.resolveSelectedOrchestratorBot(conversation);
200
- if (selectedOrchestrator) {
524
+ const selectedOrchestrator = this.resolveOrchestratorRuntimeBot(opts.orchestratorBotIdHint);
525
+ const dispatchOrchestratorBot = this.orchestratorRuntime.kind === 'clerk'
526
+ ? null
527
+ : selectedOrchestrator || null;
528
+ const hasConfiguredOrchestratorRuntime = this.orchestratorRuntime.kind === 'clerk' || !!selectedOrchestrator;
529
+ const selectedOrchestratorActor = this.getOrchestratorDispatchActor(dispatchOrchestratorBot);
530
+ if (this.orchestratorRuntime.kind === 'clerk') {
531
+ this.setLastResponseMeta({
532
+ agentName: 'Orchestrator',
533
+ botId: null,
534
+ modelLabel: this.orchestratorRuntime.modelLabel,
535
+ });
536
+ }
537
+ else if (selectedOrchestrator) {
201
538
  this.setLastResponseMeta({
202
539
  agentName: 'Orchestrator',
203
540
  botId: selectedOrchestrator.id,
204
541
  modelLabel: null,
205
542
  });
206
543
  }
207
- if (selectedOrchestrator) {
208
- orchestrationState.orchestratorBotId = selectedOrchestrator.id;
544
+ if (dispatchOrchestratorBot) {
545
+ orchestrationState.orchestratorBotId = dispatchOrchestratorBot.id;
209
546
  }
210
547
  const initialProject = conversationProjectId ? data.getProject(conversationProjectId) : undefined;
211
548
  const initialPolicy = data.getEffectiveOrchestrationPolicy(conversationProjectId || undefined);
212
- const initialAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(initialPolicy), conversationProjectId ? this.loadRoleAssignments(conversationProjectId) : {}, this.deriveRoleAssignmentsFromProject(conversationProjectId, selectedOrchestrator), promptAssignments);
549
+ const initialAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(initialPolicy), this.deriveRoleAssignmentsFromProject(conversationProjectId, dispatchOrchestratorBot), promptAssignments);
213
550
  const initialOverview = conversationProjectId ? data.getProjectOverview(conversationProjectId) : undefined;
214
551
  // If a specific workflow template was selected by the user, skip front-door classification
215
552
  // and go straight to workflow execution.
216
- if (opts.workflowTemplateId && selectedOrchestrator) {
553
+ if (opts.workflowTemplateId && hasConfiguredOrchestratorRuntime) {
217
554
  const template = data.getWorkflowTemplate(opts.workflowTemplateId);
218
555
  if (template) {
219
556
  orchestrationState.intent = 'workflow';
@@ -232,14 +569,21 @@ class OrchestratorAgent {
232
569
  const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
233
570
  if (validation.ok) {
234
571
  this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
235
- return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, initialAssignments, initialProject, orchestrationState, selectedOrchestrator, template);
572
+ if (this.isLocalDesktopRuntime()) {
573
+ const toolDispatchResult = await this.handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, dispatchOrchestratorBot, orchestrationState, initialProject, template, 'selected_workflow_template');
574
+ if (toolDispatchResult !== null) {
575
+ return toolDispatchResult;
576
+ }
577
+ return 'Orchestration could not run the selected workflow: no LLM runtime is attached to the configured orchestrator.';
578
+ }
579
+ return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, initialAssignments, initialProject, orchestrationState, selectedOrchestratorActor, template);
236
580
  }
237
581
  }
238
582
  }
239
- const namedWorkflowTemplate = selectedOrchestrator
583
+ const namedWorkflowTemplate = hasConfiguredOrchestratorRuntime
240
584
  ? this.resolveWorkflowTemplateByPrompt(conversationProjectId, prompt)
241
585
  : undefined;
242
- if (namedWorkflowTemplate && selectedOrchestrator) {
586
+ if (namedWorkflowTemplate && hasConfiguredOrchestratorRuntime) {
243
587
  orchestrationState.intent = 'workflow';
244
588
  this.recordOrchestrationAudit(orchestrationState, 'choose_path', 'path_selected', `User explicitly named workflow template: ${namedWorkflowTemplate.name}`, { workflowTemplateId: namedWorkflowTemplate.id });
245
589
  const workflowIntent = {
@@ -256,49 +600,48 @@ class OrchestratorAgent {
256
600
  const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
257
601
  if (validation.ok) {
258
602
  this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
259
- return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, initialAssignments, initialProject, orchestrationState, selectedOrchestrator, namedWorkflowTemplate);
603
+ if (this.isLocalDesktopRuntime()) {
604
+ const toolDispatchResult = await this.handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, dispatchOrchestratorBot, orchestrationState, initialProject, namedWorkflowTemplate, 'named_workflow_template');
605
+ if (toolDispatchResult !== null) {
606
+ return toolDispatchResult;
607
+ }
608
+ return 'Orchestration could not run the named workflow: no LLM runtime is attached to the configured orchestrator.';
609
+ }
610
+ return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, initialAssignments, initialProject, orchestrationState, selectedOrchestratorActor, namedWorkflowTemplate);
260
611
  }
261
612
  }
613
+ if (this.isLocalDesktopRuntime() && hasConfiguredOrchestratorRuntime) {
614
+ // Phase B of orchestration-plan.txt. For the local_desktop runtime, the
615
+ // LLM-tool-dispatch path replaces the regex-based front door. All
616
+ // preflight flows (pendingCheckpoint, pendingPolicy, workflowTemplateId,
617
+ // namedWorkflowTemplate) have already been handled above — by this
618
+ // point we are choosing between reply_directly / delegate_single /
619
+ // create_workflow via the orchestrator LLM. Other runtimes
620
+ // (connected/server) still use the legacy front door.
621
+ const toolDispatchResult = await this.handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, dispatchOrchestratorBot, orchestrationState, initialProject);
622
+ if (toolDispatchResult !== null) {
623
+ return toolDispatchResult;
624
+ }
625
+ // Preconditions unmet in local_desktop. Surface a clear error rather
626
+ // than fall through to the deprecated regex front door.
627
+ return 'Orchestration could not run: no LLM runtime is attached to the configured orchestrator. Check Settings -> Orchestration or Settings -> Bots.';
628
+ }
629
+ if (this.isLocalDesktopRuntime()) {
630
+ return 'Orchestration could not run: no orchestrator runtime is configured. Check Settings -> Orchestration or mark a bot as orchestrator in Settings -> Bots.';
631
+ }
262
632
  if (selectedOrchestrator) {
263
633
  const frontDoorResult = await this.handleOrchestratorFrontDoor(prompt, conversationId, opts, selectedOrchestrator, conversationProjectId, initialProject, initialPolicy, promptAssignments, initialAssignments, initialOverview, orchestrationState);
264
634
  if (frontDoorResult !== null)
265
635
  return frontDoorResult;
266
636
  }
267
- let intent;
268
- let routedPlan;
269
- if (selectedOrchestrator) {
270
- this.reportHiddenRoleProgress(opts, 'intent_classifier', 'Recovering routing from code fallback');
271
- intent = this.fallbackClassifyUserMessage(prompt, routingMode);
272
- orchestrationState.fallbackUsed = true;
273
- if (this.applyClassificationGuards(prompt, intent, routingMode)) {
274
- (0, state_1.markMisrouteCorrected)(orchestrationState);
275
- }
276
- this.reportHiddenRoleProgress(opts, 'intent_classifier', `Classified request as ${this.describeIntentForActivity(intent)}`);
277
- }
278
- else {
279
- this.reportHiddenRoleProgress(opts, 'intent_classifier', 'Classifying request');
280
- (0, state_1.addHelperRoleUsage)(orchestrationState, 'intent_classifier');
281
- routedPlan = await this.decomposeAndRouteUserMessage(prompt, conversationId, opts.projectId, routingMode);
282
- const routedIntent = routedPlan?.intents.length === 1
283
- ? this.buildIntentAnalysisFromRoutedIntent(routedPlan.intents[0])
284
- : undefined;
285
- if (routedIntent) {
286
- intent = routedIntent;
287
- if (this.applyClassificationGuards(prompt, intent, routingMode)) {
288
- (0, state_1.markMisrouteCorrected)(orchestrationState);
289
- }
290
- this.reportHiddenRoleProgress(opts, 'intent_classifier', `Routed single intent as ${this.describeIntentForActivity(intent)}`);
291
- }
292
- else {
293
- this.reportHiddenRoleProgress(opts, 'intent_classifier', 'Classifying request');
294
- intent = await this.classifyUserMessage(prompt, opts.projectId, routingMode);
295
- orchestrationState.fallbackUsed = true;
296
- if (this.applyClassificationGuards(prompt, intent, routingMode)) {
297
- (0, state_1.markMisrouteCorrected)(orchestrationState);
298
- }
299
- this.reportHiddenRoleProgress(opts, 'intent_classifier', `Classified request as ${this.describeIntentForActivity(intent)}`);
300
- }
637
+ this.reportHiddenRoleProgress(opts, 'intent_classifier', selectedOrchestrator ? 'Recovering routing from code fallback' : 'Classifying request');
638
+ (0, state_1.addHelperRoleUsage)(orchestrationState, 'intent_classifier');
639
+ const intent = this.fallbackClassifyUserMessage(prompt, routingMode);
640
+ orchestrationState.fallbackUsed = true;
641
+ if (this.applyClassificationGuards(prompt, intent, routingMode)) {
642
+ (0, state_1.markMisrouteCorrected)(orchestrationState);
301
643
  }
644
+ this.reportHiddenRoleProgress(opts, 'intent_classifier', `Classified request as ${this.describeIntentForActivity(intent)}`);
302
645
  orchestrationState.intent = intent.intent;
303
646
  orchestrationState.taskType = intent.primaryMode;
304
647
  this.recordOrchestrationAudit(orchestrationState, 'understand_request', 'classified', intent.reasoning || `Classified as ${intent.primaryMode}.`, {
@@ -319,23 +662,19 @@ class OrchestratorAgent {
319
662
  const effectiveProjectId = refreshedConversation?.project_id || resolvedProjectId;
320
663
  const effectiveProject = effectiveProjectId ? data.getProject(effectiveProjectId) : undefined;
321
664
  const effectivePolicy = data.getEffectiveOrchestrationPolicy(effectiveProjectId || conversationProjectId || undefined);
322
- const effectiveOrchestrator = refreshedConversation ? this.resolveSelectedOrchestratorBot(refreshedConversation) : selectedOrchestrator;
323
- const effectiveAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(effectivePolicy), effectiveProjectId ? this.loadRoleAssignments(effectiveProjectId) : {}, this.deriveRoleAssignmentsFromProject(effectiveProjectId, effectiveOrchestrator), promptAssignments);
665
+ const effectiveOrchestrator = this.resolveOrchestratorRuntimeBot(opts.orchestratorBotIdHint) || selectedOrchestrator;
666
+ const effectiveAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(effectivePolicy), this.deriveRoleAssignmentsFromProject(effectiveProjectId, effectiveOrchestrator), promptAssignments);
324
667
  if (effectiveProjectId &&
325
668
  this.hasRoleAssignments(promptAssignments) &&
326
669
  !this.hasMode(intent, 'POLICY_UPDATE')) {
327
- this.persistRoleAssignments(effectiveProjectId, conversationId, refreshedConversation?.agent_id || 'orchestrator', promptAssignments);
328
- this.persistPreferredWorkflow(effectiveProjectId, conversationId, refreshedConversation?.agent_id || 'orchestrator', effectiveAssignments);
670
+ this.persistPreferredWorkflow(effectiveProjectId, conversationId, this.orchestratorRuntime.kind === 'clerk'
671
+ ? 'clerk-orchestrator'
672
+ : (effectiveOrchestrator?.id || opts.orchestratorBotIdHint || 'orchestrator'), effectiveAssignments);
329
673
  }
330
674
  const projectOverview = effectiveProjectId ? data.getProjectOverview(effectiveProjectId) : undefined;
331
675
  if (this.isOrchestratorControlMessage(prompt)) {
332
676
  return this.formatOrchestratorControlResponse(prompt, effectiveAssignments, projectOverview);
333
677
  }
334
- if (routedPlan && routedPlan.intents.length > 1) {
335
- (0, state_1.setOrchestrationPath)(orchestrationState, 'workflow', 'workflow');
336
- this.recordOrchestrationAudit(orchestrationState, 'choose_path', 'path_selected', 'Selected ordered multi-intent workflow path.', { intentCount: routedPlan.intents.length });
337
- return this.handleOrderedIntentSequence(routedPlan, prompt, conversationId, opts, promptAssignments, effectiveProjectId, effectiveProject, effectivePolicy, effectiveAssignments, projectOverview);
338
- }
339
678
  if (this.hasMode(intent, 'POLICY_UPDATE')) {
340
679
  (0, state_1.addHelperRoleUsage)(orchestrationState, 'policy_interpreter');
341
680
  this.reportHiddenRoleProgress(opts, 'policy_interpreter', 'Reviewing requested orchestration policy changes');
@@ -474,22 +813,15 @@ class OrchestratorAgent {
474
813
  buildFrontDoorEffectivePolicy(policy, orchestratorBot, roleAssignments, taskType) {
475
814
  const normalize = (value) => String(value || '').trim().toLowerCase();
476
815
  const orchestratorName = normalize(orchestratorBot.name);
477
- const roleClass = normalize(orchestratorBot.role_class);
478
816
  const codingOwner = normalize(roleAssignments.coding);
479
817
  const qaOwner = normalize(roleAssignments.qa);
480
818
  const next = { ...policy };
481
819
  const orchestratorOwnsCoding = orchestratorName.length > 0
482
820
  && (orchestratorName === codingOwner
483
- || roleClass === 'code'
484
- || roleClass === 'coding'
485
- || roleClass === 'coder'
486
- || roleClass === 'builder'
487
- || roleClass === 'developer');
821
+ || this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer']));
488
822
  const orchestratorOwnsQa = orchestratorName.length > 0
489
823
  && (orchestratorName === qaOwner
490
- || roleClass === 'qa'
491
- || roleClass === 'review'
492
- || roleClass === 'reviewer');
824
+ || this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']));
493
825
  if (taskType === 'coding' && orchestratorOwnsCoding) {
494
826
  next.allowOrchestratorCode = true;
495
827
  }
@@ -498,437 +830,6 @@ class OrchestratorAgent {
498
830
  }
499
831
  return next;
500
832
  }
501
- async decomposeAndRouteUserMessage(prompt, conversationId, projectId, routingMode) {
502
- try {
503
- const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
504
- const firstPass = await this.clerk.decomposeUserIntents({
505
- prompt,
506
- agents,
507
- });
508
- if (!firstPass?.intents?.length)
509
- return null;
510
- let plan = firstPass;
511
- if (firstPass.intents.some((intent) => intent.needsContext)) {
512
- const recentTurnsText = this.buildIntentVerifierRecentTurns(conversationId);
513
- const verified = await this.clerk.verifyDecomposedUserIntents({
514
- prompt,
515
- priorPlan: firstPass,
516
- recentTurnsText,
517
- });
518
- if (verified?.intents?.length) {
519
- plan = verified;
520
- }
521
- }
522
- const clarificationIntents = plan.intents.filter((intent) => intent.needsClarification);
523
- if (clarificationIntents.length > 0) {
524
- return {
525
- intents: clarificationIntents.map((intent) => ({
526
- id: intent.id,
527
- request: intent.request,
528
- category: 'CLARIFICATION_NEEDED',
529
- targetAgent: undefined,
530
- dependsOn: [...intent.dependsOn],
531
- requirements: [...intent.requirements],
532
- confidence: intent.confidence || 'MEDIUM',
533
- needsClarification: true,
534
- clarificationQuestions: [...(intent.clarificationQuestions || [])],
535
- reasoning: plan.reasoning || 'Intent verification requires clarification before routing.',
536
- })),
537
- reasoning: plan.reasoning,
538
- };
539
- }
540
- const routed = [];
541
- for (const atom of plan.intents) {
542
- const routedAtom = await this.clerk.routeIntentAtom({
543
- atom,
544
- agents,
545
- routingMode,
546
- });
547
- if (!routedAtom)
548
- return null;
549
- routed.push(routedAtom);
550
- }
551
- const collapsed = this.collapseSingleWorkerPlan(prompt, routed);
552
- return {
553
- intents: collapsed,
554
- reasoning: plan.reasoning,
555
- };
556
- }
557
- catch {
558
- return null;
559
- }
560
- }
561
- buildIntentVerifierRecentTurns(conversationId) {
562
- try {
563
- const context = (0, context_window_1.getPromptContextWindow)(conversationId, 3);
564
- return (0, context_window_1.formatTurnsForPrompt)(context.turns);
565
- }
566
- catch {
567
- return '(no recent messages)';
568
- }
569
- }
570
- collapseSingleWorkerPlan(prompt, intents) {
571
- if (intents.length <= 1)
572
- return intents;
573
- if (this.hasExplicitOrderingLanguage(prompt))
574
- return intents;
575
- if (!this.hasConcreteArtifactDetails(prompt))
576
- return intents;
577
- if (this.countMentionedAgents(prompt) > 1)
578
- return intents;
579
- if (!intents.every((intent) => intent.category === 'ASK_WORKER' || intent.category === 'FULL_WORKFLOW'))
580
- return intents;
581
- const resolvedTargets = Array.from(new Set(intents
582
- .map((intent) => (intent.targetAgent || '').trim().toLowerCase())
583
- .filter(Boolean)));
584
- if (resolvedTargets.length > 1 && this.countMentionedAgents(prompt) > 0)
585
- return intents;
586
- const mergedRequirements = Array.from(new Set(intents
587
- .flatMap((intent) => [intent.request, ...intent.requirements])
588
- .map((item) => item.trim())
589
- .filter(Boolean)));
590
- const collapsedConfidence = intents.some((intent) => (intent.confidence || 'MEDIUM') === 'LOW')
591
- ? 'LOW'
592
- : intents.some((intent) => (intent.confidence || 'MEDIUM') === 'MEDIUM')
593
- ? 'MEDIUM'
594
- : 'HIGH';
595
- return [{
596
- ...intents[0],
597
- category: 'ASK_WORKER',
598
- request: prompt.trim(),
599
- targetAgent: this.countMentionedAgents(prompt) > 0
600
- ? intents.find((intent) => intent.targetAgent)?.targetAgent
601
- : undefined,
602
- dependsOn: [],
603
- requirements: mergedRequirements,
604
- confidence: collapsedConfidence,
605
- needsClarification: intents.some((intent) => intent.needsClarification),
606
- clarificationQuestions: Array.from(new Set(intents.flatMap((intent) => intent.clarificationQuestions || []))),
607
- reasoning: 'Collapsed multiple single-worker fragments into one deliverable for one worker.',
608
- }];
609
- }
610
- hasExplicitOrderingLanguage(prompt) {
611
- return /\b(before|after|then|first|last|next|once that is done|once done|if that succeeds|after that|follow the workflow|workflow|todo|handoff)\b/i.test(prompt);
612
- }
613
- buildIntentAnalysisFromRoutedIntent(routed) {
614
- let primaryMode = 'DIRECT_CONVERSATION';
615
- let userFacingMode = 'DIRECT_RESPONSE';
616
- let targetScope = 'SELF';
617
- let isMultiStep = false;
618
- switch (routed.category) {
619
- case 'ASK_WORKER':
620
- primaryMode = 'PROXY_MODE';
621
- userFacingMode = 'ASK_WORKER';
622
- targetScope = 'ONE_WORKER';
623
- break;
624
- case 'STATUS_CHECK':
625
- primaryMode = 'STATUS_INQUIRY';
626
- userFacingMode = 'STATUS_CHECK';
627
- break;
628
- case 'POLICY_CONFIGURATION':
629
- primaryMode = 'POLICY_UPDATE';
630
- userFacingMode = 'POLICY_CONFIGURATION';
631
- break;
632
- case 'FULL_WORKFLOW':
633
- primaryMode = 'WORKFLOW_MODE';
634
- userFacingMode = 'FULL_WORKFLOW';
635
- targetScope = 'MULTI_WORKER';
636
- isMultiStep = true;
637
- break;
638
- case 'MEMORY_CAPTURE':
639
- primaryMode = 'MEMORY_CAPTURE';
640
- userFacingMode = 'DIRECT_RESPONSE';
641
- break;
642
- default:
643
- primaryMode = 'DIRECT_CONVERSATION';
644
- userFacingMode = 'DIRECT_RESPONSE';
645
- break;
646
- }
647
- const intent = this.inferIntentFromRequest(routed.request, routed.category);
648
- const targetAgent = routed.targetAgent;
649
- return {
650
- primaryMode,
651
- secondaryModes: [],
652
- executionOrder: [primaryMode],
653
- userFacingMode,
654
- targetScope,
655
- confidence: routed.confidence,
656
- intent,
657
- projectMatch: undefined,
658
- topicMatch: undefined,
659
- targetAgent,
660
- isMultiStep,
661
- reasoning: routed.reasoning,
662
- needsClarification: routed.category === 'CLARIFICATION_NEEDED' || routed.needsClarification,
663
- clarificationQuestions: routed.clarificationQuestions,
664
- };
665
- }
666
- inferIntentFromRequest(request, category) {
667
- const normalized = request.toLowerCase();
668
- if (category === 'POLICY_CONFIGURATION')
669
- return 'plan';
670
- if (category === 'STATUS_CHECK')
671
- return 'question';
672
- if (/\b(brainstorm|research|pressure-test|feasibility|is this a good idea|is this sound|evaluate this idea)\b/i.test(normalized)) {
673
- return 'brainstorm';
674
- }
675
- if (/\b(review|qa|test|audit|verify)\b/i.test(normalized))
676
- return 'review';
677
- if (/\b(plan|outline|roadmap|approach)\b/i.test(normalized))
678
- return 'plan';
679
- if (/\b(build|code|implement|create|fix|write|update)\b/i.test(normalized))
680
- return 'build';
681
- if (/\b(what|how|why|when|where|show|list|find|check)\b/i.test(normalized))
682
- return 'question';
683
- return category === 'ASK_WORKER' ? 'discuss' : 'simple';
684
- }
685
- async handleOrderedIntentSequence(routedPlan, originalPrompt, conversationId, opts, promptAssignments, effectiveProjectId, effectiveProject, effectivePolicy, effectiveAssignments, projectOverview) {
686
- const normalizedIntents = this.normalizeOrderedIntentSequence(originalPrompt, routedPlan.intents, effectiveAssignments);
687
- this.reportHiddenRoleProgress(opts, 'intent_classifier', `Decomposed request into ${normalizedIntents.length} ordered intents`);
688
- if (normalizedIntents.length > 1) {
689
- opts.onProgress(this.formatOrderedIntentPlanPreview(normalizedIntents));
690
- }
691
- const persistedPlan = data.createIntentExecutionPlan({
692
- conversationId,
693
- sourcePrompt: originalPrompt,
694
- reasoning: routedPlan.reasoning,
695
- intents: normalizedIntents.map((intent, index) => ({
696
- atomId: intent.id,
697
- orderIndex: index + 1,
698
- request: intent.request,
699
- category: intent.category,
700
- targetAgent: intent.targetAgent,
701
- requirements: intent.requirements,
702
- dependsOn: intent.dependsOn,
703
- confidence: intent.confidence,
704
- needsClarification: intent.needsClarification,
705
- clarificationQuestions: intent.clarificationQuestions,
706
- reasoning: intent.reasoning,
707
- })),
708
- });
709
- data.updateIntentExecutionPlan(persistedPlan.id, { status: 'running' });
710
- const lines = [];
711
- let completedIntents = 0;
712
- for (let index = 0; index < normalizedIntents.length; index += 1) {
713
- const routed = normalizedIntents[index];
714
- const persistedAtom = persistedPlan.atoms[index];
715
- if (routed.category === 'CLARIFICATION_NEEDED' || routed.needsClarification) {
716
- if (persistedAtom) {
717
- data.updateIntentExecutionAtom(persistedAtom.id, {
718
- status: 'blocked',
719
- error: 'Clarification required before execution.',
720
- });
721
- }
722
- data.updateIntentExecutionPlan(persistedPlan.id, {
723
- status: 'blocked',
724
- completedIntents,
725
- });
726
- const clarificationIntent = this.buildIntentAnalysisFromRoutedIntent(routed);
727
- return this.formatClarificationResponse(clarificationIntent, projectOverview);
728
- }
729
- const intent = this.buildIntentAnalysisFromRoutedIntent(routed);
730
- if (persistedAtom) {
731
- data.updateIntentExecutionAtom(persistedAtom.id, {
732
- status: 'running',
733
- startedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
734
- error: null,
735
- });
736
- }
737
- const executionValidation = this.validateIntentExecution(routed.request, intent, effectivePolicy);
738
- if (!executionValidation.ok) {
739
- if (persistedAtom) {
740
- data.updateIntentExecutionAtom(persistedAtom.id, {
741
- status: 'failed',
742
- error: executionValidation.reason,
743
- });
744
- }
745
- data.updateIntentExecutionPlan(persistedPlan.id, {
746
- status: 'failed',
747
- completedIntents,
748
- });
749
- if (executionValidation.clarificationQuestions?.length) {
750
- return this.formatClarificationResponse({ ...intent, clarificationQuestions: executionValidation.clarificationQuestions }, projectOverview);
751
- }
752
- return `I couldn't execute step ${index + 1} safely: ${executionValidation.reason}`;
753
- }
754
- const executionSpec = executionValidation.executionSpec;
755
- const stepHeader = `**Step ${index + 1}:** ${routed.request}`;
756
- if (this.hasMode(intent, 'POLICY_UPDATE')) {
757
- const policyResponse = await this.handlePolicyUpdate(routed.request, conversationId, intent, this.parseRoleAssignments(routed.request), effectiveProjectId, projectOverview, effectivePolicy);
758
- if (persistedAtom) {
759
- data.updateIntentExecutionAtom(persistedAtom.id, {
760
- status: 'completed',
761
- resultSummary: this.summarizeResultForIntentPlan(policyResponse),
762
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
763
- });
764
- }
765
- completedIntents += 1;
766
- if (index < routedPlan.intents.length - 1) {
767
- data.updateIntentExecutionPlan(persistedPlan.id, {
768
- status: 'blocked',
769
- completedIntents,
770
- });
771
- return `${policyResponse}\n\nI paused the remaining ordered steps until the policy change is confirmed.`;
772
- }
773
- data.updateIntentExecutionPlan(persistedPlan.id, {
774
- status: 'completed',
775
- completedIntents,
776
- finishedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
777
- });
778
- return policyResponse;
779
- }
780
- if (this.hasMode(intent, 'STATUS_INQUIRY')) {
781
- const statusResponse = this.formatStatusInquiryResponse(conversationId, effectiveProjectId);
782
- if (persistedAtom) {
783
- data.updateIntentExecutionAtom(persistedAtom.id, {
784
- status: 'completed',
785
- resultSummary: this.summarizeResultForIntentPlan(statusResponse),
786
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
787
- });
788
- }
789
- completedIntents += 1;
790
- lines.push(stepHeader);
791
- lines.push(statusResponse);
792
- continue;
793
- }
794
- if (this.hasMode(intent, 'MEMORY_CAPTURE')) {
795
- const memoryResponse = this.formatMemoryCaptureResponse(routed.request);
796
- if (persistedAtom) {
797
- data.updateIntentExecutionAtom(persistedAtom.id, {
798
- status: 'completed',
799
- resultSummary: this.summarizeResultForIntentPlan(memoryResponse),
800
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
801
- });
802
- }
803
- completedIntents += 1;
804
- lines.push(stepHeader);
805
- lines.push(memoryResponse);
806
- continue;
807
- }
808
- if (executionSpec.executionMode === 'direct') {
809
- const directResponse = this.handleDirectConversation(routed.request, projectOverview, effectiveAssignments);
810
- if (persistedAtom) {
811
- data.updateIntentExecutionAtom(persistedAtom.id, {
812
- status: 'completed',
813
- resultSummary: this.summarizeResultForIntentPlan(directResponse),
814
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
815
- });
816
- }
817
- completedIntents += 1;
818
- lines.push(stepHeader);
819
- lines.push(directResponse);
820
- continue;
821
- }
822
- if (executionSpec.executionMode === 'proxy') {
823
- const proxyPrompt = this.buildSimpleWorkerPrompt(routed.request, executionSpec.targetAgents[0] || intent.targetAgent || 'the worker');
824
- const proxyResult = await this.executeProxyRequest(proxyPrompt, conversationId, intent, opts, executionSpec, effectiveAssignments);
825
- const proxyResponse = proxyResult.response;
826
- if (!proxyResult.ok) {
827
- if (persistedAtom) {
828
- data.updateIntentExecutionAtom(persistedAtom.id, {
829
- status: 'failed',
830
- error: proxyResult.error || proxyResponse,
831
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
832
- });
833
- }
834
- data.updateIntentExecutionPlan(persistedPlan.id, {
835
- status: 'failed',
836
- completedIntents,
837
- finishedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
838
- });
839
- lines.push(stepHeader);
840
- lines.push(proxyResponse);
841
- return lines.join('\n\n');
842
- }
843
- if (persistedAtom) {
844
- data.updateIntentExecutionAtom(persistedAtom.id, {
845
- status: 'completed',
846
- resultSummary: this.summarizeResultForIntentPlan(proxyResponse),
847
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
848
- });
849
- }
850
- completedIntents += 1;
851
- lines.push(stepHeader);
852
- lines.push(proxyResponse);
853
- continue;
854
- }
855
- if (executionSpec.executionMode === 'workflow') {
856
- const workflowResponse = await this.handleOrchestratedRequest(routed.request, conversationId, intent, executionSpec, opts, effectiveAssignments, effectiveProject);
857
- if (persistedAtom) {
858
- data.updateIntentExecutionAtom(persistedAtom.id, {
859
- status: 'completed',
860
- resultSummary: this.summarizeResultForIntentPlan(workflowResponse),
861
- completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
862
- });
863
- }
864
- completedIntents += 1;
865
- lines.push(stepHeader);
866
- lines.push(workflowResponse);
867
- }
868
- }
869
- data.updateIntentExecutionPlan(persistedPlan.id, {
870
- status: 'completed',
871
- completedIntents,
872
- finishedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
873
- });
874
- return lines.join('\n\n');
875
- }
876
- normalizeOrderedIntentSequence(prompt, intents, assignments) {
877
- const collapsed = this.collapseSingleWorkerPlan(prompt, intents);
878
- const normalized = collapsed.map((intent) => this.applyImplicitWorkerTarget(intent, assignments));
879
- return this.dropRedundantSequentialWorkerHandoffs(normalized);
880
- }
881
- applyImplicitWorkerTarget(intent, assignments) {
882
- if (intent.category !== 'ASK_WORKER' || intent.targetAgent || intent.needsClarification) {
883
- return intent;
884
- }
885
- const targetAgent = this.resolveImplicitWorkerTarget(intent.request, intent.requirements, assignments);
886
- return targetAgent ? { ...intent, targetAgent } : intent;
887
- }
888
- resolveImplicitWorkerTarget(request, requirements, assignments) {
889
- const text = `${request}\n${requirements.join('\n')}`.toLowerCase();
890
- if (/\b(research|brainstorm|feasibility|evaluate|pressure-test|is this sound|idea)\b/i.test(text)) {
891
- return assignments.research || assignments.coding || assignments.qa;
892
- }
893
- if (/\b(qa|review|verify|test|audit|check)\b/i.test(text)) {
894
- return assignments.qa || assignments.coding || assignments.research;
895
- }
896
- if (/\b(build|code|implement|create|fix|write|update|design)\b/i.test(text)) {
897
- return assignments.coding || assignments.qa || assignments.research;
898
- }
899
- return assignments.coding || assignments.research || assignments.qa;
900
- }
901
- dropRedundantSequentialWorkerHandoffs(intents) {
902
- if (intents.length <= 1)
903
- return intents;
904
- const filtered = [];
905
- for (let index = 0; index < intents.length; index += 1) {
906
- const current = intents[index];
907
- const next = intents[index + 1];
908
- const currentText = current.request.toLowerCase();
909
- const nextText = next?.request.toLowerCase() || '';
910
- const redundantFeedbackStep = Boolean(next &&
911
- current.category === 'ASK_WORKER' &&
912
- next.category === 'ASK_WORKER' &&
913
- current.targetAgent &&
914
- next.targetAgent &&
915
- current.targetAgent.toLowerCase() === next.targetAgent.toLowerCase() &&
916
- /\b(give|send|provide|pass along)\b.*\b(feedback|qa)\b/.test(currentText) &&
917
- /\b(fix|implement|apply|address)\b/.test(nextText));
918
- if (!redundantFeedbackStep) {
919
- filtered.push(current);
920
- }
921
- }
922
- return filtered;
923
- }
924
- formatOrderedIntentPlanPreview(intents) {
925
- const lines = [`**Here's my plan (${intents.length} steps):**`];
926
- intents.forEach((intent, index) => {
927
- const agentLabel = intent.targetAgent ? ` (${intent.targetAgent})` : '';
928
- lines.push(`${index + 1}. ${intent.request}${agentLabel}`);
929
- });
930
- return lines.join('\n');
931
- }
932
833
  shouldForceProxy(prompt, intent) {
933
834
  if (intent.primaryMode === 'PROXY_MODE')
934
835
  return true;
@@ -942,67 +843,16 @@ class OrchestratorAgent {
942
843
  const mentionedAgents = this.countMentionedAgents(prompt);
943
844
  return mentionedAgents === 1 && /\b(ask|let|have|talk to|discuss with|check with)\b/i.test(prompt);
944
845
  }
945
- applyClassificationGuards(prompt, intent, routingMode) {
946
- const before = JSON.stringify({
947
- primaryMode: intent.primaryMode,
948
- targetAgent: intent.targetAgent || null,
949
- needsClarification: !!intent.needsClarification,
950
- clarificationQuestions: intent.clarificationQuestions || [],
951
- });
952
- const targetAgent = this.extractTargetAgentName(prompt) || intent.targetAgent;
953
- const needsConcreteArtifact = /\b(this idea|review this idea|code it|qa it|review it|build it|fix it)\b/i.test(prompt)
954
- && !this.hasConcreteArtifactDetails(prompt);
955
- const explicitWorkflowRoles = this.inferRequestedWorkflowRoles(prompt, {});
956
- const explicitWorkflowRequest = explicitWorkflowRoles.length >= 2 && this.hasConcreteArtifactDetails(prompt);
957
- if (targetAgent && !intent.targetAgent) {
958
- intent.targetAgent = targetAgent;
959
- }
960
- if (explicitWorkflowRequest) {
961
- intent.primaryMode = 'WORKFLOW_MODE';
962
- intent.userFacingMode = 'FULL_WORKFLOW';
963
- intent.targetScope = 'MULTI_WORKER';
964
- intent.isMultiStep = true;
965
- intent.intent = 'build';
966
- intent.targetAgent = undefined;
967
- intent.needsClarification = false;
968
- intent.clarificationQuestions = undefined;
969
- intent.secondaryModes = intent.secondaryModes.filter((mode) => mode !== 'PROXY_MODE');
970
- intent.executionOrder = ['WORKFLOW_MODE'];
971
- }
972
- if (intent.primaryMode === 'PROXY_MODE' && !intent.targetAgent) {
973
- intent.needsClarification = true;
974
- intent.clarificationQuestions = Array.from(new Set([
975
- ...(intent.clarificationQuestions || []),
976
- 'Which worker should handle this request?',
977
- ]));
978
- }
979
- if (needsConcreteArtifact &&
980
- (intent.primaryMode === 'WORKFLOW_MODE' || intent.primaryMode === 'PROXY_MODE')) {
981
- intent.needsClarification = true;
982
- intent.clarificationQuestions = Array.from(new Set([
983
- ...(intent.clarificationQuestions || []),
984
- 'What specific idea, artifact, or content should the team work on?',
985
- ]));
986
- }
987
- if (intent.primaryMode === 'WORKFLOW_MODE' && !intent.isMultiStep && intent.targetScope !== 'MULTI_WORKER') {
988
- intent.needsClarification = true;
989
- intent.clarificationQuestions = Array.from(new Set([
990
- ...(intent.clarificationQuestions || []),
991
- 'What multi-step workflow do you want me to coordinate?',
992
- ]));
993
- }
994
- if (intent.needsClarification && this.hasConcreteArtifactDetails(prompt)) {
995
- const remaining = (intent.clarificationQuestions || []).filter((question) => !/specific idea, artifact, or content/i.test(question));
996
- intent.clarificationQuestions = remaining.length > 0 ? remaining : undefined;
997
- intent.needsClarification = Boolean(intent.clarificationQuestions?.length);
998
- }
999
- const after = JSON.stringify({
1000
- primaryMode: intent.primaryMode,
1001
- targetAgent: intent.targetAgent || null,
1002
- needsClarification: !!intent.needsClarification,
1003
- clarificationQuestions: intent.clarificationQuestions || [],
1004
- });
1005
- return before !== after;
846
+ /**
847
+ * DEPRECATED returns false. Previously 70+ lines of regex that
848
+ * post-mutated an IntentAnalysis based on prose signals (bot names,
849
+ * role keywords, workflow language, concrete-artifact heuristics).
850
+ * Codex audit HIGH #5 (2026-04-19): runtime must not override mode/
851
+ * route via regex. The orchestrator LLM decides via structured tool
852
+ * calls (delegate_single / create_workflow / reply_directly).
853
+ */
854
+ applyClassificationGuards(_prompt, _intent, _routingMode) {
855
+ return false;
1006
856
  }
1007
857
  formatOrchestratorControlResponse(prompt, assignments, overview) {
1008
858
  const lines = [];
@@ -1056,49 +906,17 @@ class OrchestratorAgent {
1056
906
  return [agent.name.trim()].filter(Boolean);
1057
907
  }
1058
908
  /**
1059
- * Interpret user intent using the clerk.
1060
- * This is the structured hidden-role classifier for Phase 1.
909
+ * DEPRECATED returns a neutral DIRECT_CONVERSATION intent. Previously
910
+ * 80+ lines of regex on the user prompt classifying into
911
+ * POLICY_UPDATE / MEMORY_CAPTURE / STATUS_INQUIRY / WORKFLOW_MODE /
912
+ * PROXY_MODE / DIRECT_CONVERSATION based on bot names and role
913
+ * keywords. Codex audit HIGH #5 (2026-04-19): runtime must not
914
+ * infer mode/route from prose. This fallback only runs when the
915
+ * primary LLM classifier is unavailable (non-local_desktop paths);
916
+ * returning a neutral default routes to the orchestrator's direct
917
+ * handling rather than miscategorizing via regex.
1061
918
  */
1062
- async classifyUserMessage(prompt, projectId, routingMode) {
1063
- try {
1064
- const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
1065
- const projects = data.listProjects();
1066
- const topics = data.listTopics({ limit: 20 });
1067
- const parsed = await this.clerk.classifyOrchestratorIntent({
1068
- prompt,
1069
- agents,
1070
- projects,
1071
- topics,
1072
- projectId,
1073
- routingMode,
1074
- });
1075
- if (parsed) {
1076
- const primaryMode = this.normalizeMode(parsed.primaryMode);
1077
- const secondaryModes = this.normalizeModeList(parsed.secondaryModes);
1078
- const executionOrder = this.normalizeModeList(parsed.executionOrder);
1079
- return {
1080
- primaryMode,
1081
- secondaryModes,
1082
- executionOrder: executionOrder.length > 0
1083
- ? executionOrder
1084
- : [primaryMode, ...secondaryModes.filter((mode) => mode !== primaryMode)],
1085
- userFacingMode: parsed.userFacingMode,
1086
- targetScope: parsed.targetScope,
1087
- confidence: parsed.confidence,
1088
- intent: parsed.intent || 'simple',
1089
- projectMatch: undefined,
1090
- topicMatch: undefined,
1091
- targetAgent: parsed.targetAgent || undefined,
1092
- isMultiStep: Boolean(parsed.isMultiStep),
1093
- reasoning: parsed.reasoning || '',
1094
- needsClarification: Boolean(parsed.needsClarification),
1095
- clarificationQuestions: Array.isArray(parsed.clarificationQuestions) ? parsed.clarificationQuestions.map(String) : undefined,
1096
- };
1097
- }
1098
- }
1099
- catch { /* fall through */ }
1100
- return this.fallbackClassifyUserMessage(prompt, routingMode);
1101
- // Default: treat as simple single-step
919
+ fallbackClassifyUserMessage(_prompt, _routingMode) {
1102
920
  return {
1103
921
  primaryMode: 'DIRECT_CONVERSATION',
1104
922
  secondaryModes: [],
@@ -1107,106 +925,13 @@ class OrchestratorAgent {
1107
925
  targetScope: 'SELF',
1108
926
  confidence: 'LOW',
1109
927
  intent: 'simple',
1110
- isMultiStep: false,
1111
- reasoning: 'Intent analysis failed - defaulting to direct conversation',
1112
- };
1113
- }
1114
- normalizeMode(raw) {
1115
- const value = String(raw || '').trim().toUpperCase();
1116
- switch (value) {
1117
- case 'POLICY_UPDATE':
1118
- case 'MEMORY_CAPTURE':
1119
- case 'STATUS_INQUIRY':
1120
- case 'PROXY_MODE':
1121
- case 'WORKFLOW_MODE':
1122
- return value;
1123
- default:
1124
- return 'DIRECT_CONVERSATION';
1125
- }
1126
- }
1127
- normalizeModeList(raw) {
1128
- if (!Array.isArray(raw))
1129
- return [];
1130
- return raw
1131
- .map((item) => this.normalizeMode(item))
1132
- .filter((mode, index, list) => list.indexOf(mode) === index);
1133
- }
1134
- fallbackClassifyUserMessage(prompt, routingMode) {
1135
- const normalized = prompt.toLowerCase();
1136
- const targetAgent = this.extractTargetAgentName(prompt);
1137
- const hasPolicyLanguage = this.isExplicitPolicyMessage(prompt);
1138
- const hasMemoryLanguage = /\b(i am going to give you|things to remember|remember this|save this as a rule|project rules)\b/i.test(prompt);
1139
- const isStatus = /\b(what is happening|status|what is going on|what's going on|where are we|progress update|keep me updated)\b/i.test(prompt);
1140
- const isGreeting = /\b(hello|hi|hey|are you there|good morning|good afternoon|good evening)\b/i.test(prompt);
1141
- const needsWorkflow = /\b(todo|workflow|handoff|qa\b|review after|send .* then|first .* then|team workflow|multiple llms)\b/i.test(normalized)
1142
- || (/\bbrain\b/i.test(prompt) && /\bben\b/i.test(prompt))
1143
- || (/\bben\b/i.test(prompt) && /\bjohn\b/i.test(prompt));
1144
- const isProxy = Boolean(targetAgent) || (routingMode === 'proxy' && !hasPolicyLanguage && !isStatus);
1145
- let primaryMode = 'DIRECT_CONVERSATION';
1146
- const secondaryModes = [];
1147
- let intent = 'simple';
1148
- let isMultiStep = false;
1149
- if (hasPolicyLanguage) {
1150
- primaryMode = 'POLICY_UPDATE';
1151
- intent = 'plan';
1152
- secondaryModes.push('DIRECT_CONVERSATION');
1153
- }
1154
- else if (hasMemoryLanguage) {
1155
- primaryMode = 'MEMORY_CAPTURE';
1156
- }
1157
- else if (isStatus) {
1158
- primaryMode = 'STATUS_INQUIRY';
1159
- intent = 'question';
1160
- }
1161
- else if (needsWorkflow) {
1162
- primaryMode = 'WORKFLOW_MODE';
1163
- intent = 'build';
1164
- isMultiStep = true;
1165
- }
1166
- else if (isProxy) {
1167
- primaryMode = 'PROXY_MODE';
1168
- intent = 'discuss';
1169
- }
1170
- else if (isGreeting || this.isProjectKnowledgeRequest(prompt) || this.isOrchestratorControlMessage(prompt)) {
1171
- primaryMode = 'DIRECT_CONVERSATION';
1172
- intent = 'question';
1173
- }
1174
- const executionOrder = [primaryMode];
1175
- if (hasPolicyLanguage && needsWorkflow && !secondaryModes.includes('WORKFLOW_MODE')) {
1176
- secondaryModes.push('WORKFLOW_MODE');
1177
- executionOrder.push('WORKFLOW_MODE');
1178
- }
1179
- return {
1180
- primaryMode,
1181
- secondaryModes,
1182
- executionOrder,
1183
- userFacingMode: primaryMode === 'POLICY_UPDATE'
1184
- ? 'POLICY_CONFIGURATION'
1185
- : primaryMode === 'STATUS_INQUIRY'
1186
- ? 'STATUS_CHECK'
1187
- : primaryMode === 'PROXY_MODE'
1188
- ? 'ASK_WORKER'
1189
- : primaryMode === 'WORKFLOW_MODE'
1190
- ? 'FULL_WORKFLOW'
1191
- : intent === 'plan'
1192
- ? 'PLANNING'
1193
- : 'DIRECT_RESPONSE',
1194
- targetScope: primaryMode === 'PROXY_MODE'
1195
- ? 'ONE_WORKER'
1196
- : primaryMode === 'WORKFLOW_MODE'
1197
- ? 'MULTI_WORKER'
1198
- : 'SELF',
1199
- confidence: hasPolicyLanguage || isStatus || needsWorkflow || isProxy ? 'MEDIUM' : 'LOW',
1200
- intent,
1201
928
  projectMatch: undefined,
1202
929
  topicMatch: undefined,
1203
- targetAgent,
1204
- isMultiStep,
1205
- reasoning: 'Fallback intent classification',
1206
- needsClarification: primaryMode === 'PROXY_MODE' && this.isVagueProxyPrompt(prompt),
1207
- clarificationQuestions: primaryMode === 'PROXY_MODE' && this.isVagueProxyPrompt(prompt)
1208
- ? [`What specific idea, artifact, or content should ${targetAgent || 'the specialist'} review?`]
1209
- : undefined,
930
+ targetAgent: undefined,
931
+ isMultiStep: false,
932
+ reasoning: 'Fallback classifier deprecated — neutral direct-conversation default (Codex audit HIGH #5).',
933
+ needsClarification: false,
934
+ clarificationQuestions: undefined,
1210
935
  };
1211
936
  }
1212
937
  isExplicitPolicyMessage(prompt) {
@@ -1312,6 +1037,7 @@ class OrchestratorAgent {
1312
1037
  this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Routing readiness ping to ${agent.name}`);
1313
1038
  const result = await this.workflowEngine.execute(readinessPrompt, conversationId, agent.id, {
1314
1039
  apiKey: this.resolveApiKey(agent.provider),
1040
+ abortSignal: opts.abortSignal,
1315
1041
  disableDecomposition: true,
1316
1042
  });
1317
1043
  if (result.status === 'completed' || result.status === 'partial') {
@@ -1525,14 +1251,686 @@ class OrchestratorAgent {
1525
1251
  .replace(/\n?STATUS:\s*(PASS|FAIL|COMPLETE|BLOCKED|UNKNOWN)\s*$/i, '')
1526
1252
  .trim() || rawText.trim();
1527
1253
  }
1528
- resolveSelectedOrchestratorBot(conversation) {
1529
- const conversationBotId = conversation?.agent_id;
1530
- if (conversationBotId) {
1531
- const bot = data.getAgentProfile(conversationBotId);
1254
+ /** Summarize a TodoTaskRow.status map ({participant: completed}) into a single label. */
1255
+ // NOTE: module-level helper hoisted below the class; declared here as a forward reference only.
1256
+ summarizeTodoStatus(status) {
1257
+ if (!status || typeof status !== 'object')
1258
+ return 'queued';
1259
+ const values = Object.values(status);
1260
+ if (values.length === 0)
1261
+ return 'queued';
1262
+ if (values.every((v) => v === true))
1263
+ return 'completed';
1264
+ if (values.some((v) => v === true))
1265
+ return 'in_progress';
1266
+ return 'queued';
1267
+ }
1268
+ resolveOrchestratorRuntimeBot(orchestratorBotIdHint) {
1269
+ const hintedBotId = String(orchestratorBotIdHint || '').trim();
1270
+ if (hintedBotId) {
1271
+ const bot = data.getAgentProfile(hintedBotId);
1532
1272
  if (bot)
1533
1273
  return bot;
1534
1274
  }
1535
- return data.getOrchestratorBot() || data.getDefaultAgentProfile();
1275
+ // Orchestrator must be explicitly designated via is_orchestrator=1.
1276
+ // No fallback to getDefaultAgentProfile() — that's the hardcoded-Ben trap that
1277
+ // made the default bot double as orchestrator and worker simultaneously.
1278
+ return data.getOrchestratorBot() || undefined;
1279
+ }
1280
+ getOrchestratorDispatchActor(orchestratorBot) {
1281
+ if (orchestratorBot) {
1282
+ return { name: orchestratorBot.name, actorId: orchestratorBot.id, bot: orchestratorBot };
1283
+ }
1284
+ return {
1285
+ name: this.orchestratorRuntime.agentName || 'Orchestrator',
1286
+ actorId: this.orchestratorRuntime.botId || 'clerk-orchestrator',
1287
+ };
1288
+ }
1289
+ /**
1290
+ * Phase A wire-up for orchestration-plan.txt.
1291
+ * Called when `orchestration.tool_dispatch_enabled` flag is ON.
1292
+ * Builds the hint, calls the orchestrator LLM with the three dispatch
1293
+ * tools exposed, validates the tool call, and executes it via the
1294
+ * pure dispatch-executor (creates TODOs or returns a direct reply).
1295
+ *
1296
+ * Returns the user-facing reply string on success. Returns `null` if
1297
+ * preconditions are not met (no orchestrator bot configured, no LLM
1298
+ * runtime available) so the caller can fall back to the legacy path.
1299
+ */
1300
+ async handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, orchestratorBot, orchestrationState, initialProject, selectedWorkflowTemplate, selectedWorkflowSource = 'selected_workflow_template') {
1301
+ // Dynamic imports — keep dispatch modules isolated.
1302
+ const { runDispatchTurn } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-runner')));
1303
+ const { executeCreateWorkflow } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-executor')));
1304
+ const { computePlanExecutionIntent } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-hint')));
1305
+ // Get an LLM interface to call. For the local_desktop runtime, the
1306
+ // orchestratorRuntime's llm is what we need; for clerk runtime, same.
1307
+ const llm = this.orchestratorRuntime?.llm;
1308
+ if (!llm || typeof llm.chat !== 'function') {
1309
+ return null;
1310
+ }
1311
+ const project = conversationProjectId ? data.getProject(conversationProjectId) : undefined;
1312
+ const effectivePolicy = data.getEffectiveOrchestrationPolicy(conversationProjectId || undefined);
1313
+ const effectivePolicyText = (0, policy_prompt_1.buildEffectivePolicyPromptSection)(effectivePolicy, {
1314
+ heading: '',
1315
+ defaultLine: 'No confirmed special policy is set.',
1316
+ });
1317
+ // Candidate worker bots for dispatch: project-scoped and orchestrator-
1318
+ // profiles excluded (Codex QA 2026-04-18, finding #2). Using
1319
+ // listProjectScopedBots ensures role resolution cannot assign work to
1320
+ // a bot that isn't in the current project roster, and cannot pick
1321
+ // the orchestrator itself as a worker.
1322
+ const activeBots = this.listProjectScopedBots(conversationProjectId || undefined, { includeOrchestratorProfiles: !orchestratorBot })
1323
+ .filter((b) => b.is_active === 1 && (!orchestratorBot || b.id !== orchestratorBot.id));
1324
+ if (selectedWorkflowTemplate) {
1325
+ const existingIds = new Set(activeBots.map((bot) => bot.id));
1326
+ for (const step of selectedWorkflowTemplate.steps) {
1327
+ const bot = data.getAgentProfile(step.agent_id);
1328
+ if (bot && bot.is_active === 1 && !existingIds.has(bot.id)) {
1329
+ activeBots.push(bot);
1330
+ existingIds.add(bot.id);
1331
+ }
1332
+ }
1333
+ }
1334
+ const dispatchHintBots = activeBots
1335
+ .filter((bot) => bot.is_active === 1)
1336
+ .map((bot) => ({
1337
+ name: bot.name,
1338
+ provider: bot.provider,
1339
+ model: bot.model || null,
1340
+ role_priorities: data.getAgentRolePriorities(bot),
1341
+ is_active: bot.is_active === 1,
1342
+ }));
1343
+ const planExecutionIntent = computePlanExecutionIntent(prompt, dispatchHintBots);
1344
+ const selectedWorkflowTemplateHint = selectedWorkflowTemplate
1345
+ ? this.buildSelectedWorkflowTemplateHint(selectedWorkflowTemplate)
1346
+ : null;
1347
+ const explicitCandidatePlan = selectedWorkflowTemplateHint || planExecutionIntent.mode !== 'none'
1348
+ ? null
1349
+ : this.buildExplicitBotSequenceCandidatePlan(prompt, activeBots);
1350
+ const candidateWorkflowPlan = planExecutionIntent.mode !== 'none'
1351
+ ? null
1352
+ : selectedWorkflowTemplateHint
1353
+ ? {
1354
+ source: selectedWorkflowSource,
1355
+ reason: selectedWorkflowSource === 'named_workflow_template'
1356
+ ? `User named workflow "${selectedWorkflowTemplate.name}".`
1357
+ : `User selected workflow "${selectedWorkflowTemplate.name}".`,
1358
+ steps: selectedWorkflowTemplateHint.steps.map((step) => ({
1359
+ index: step.index,
1360
+ bot_name: step.bot_name,
1361
+ role_priorities: step.role_priorities,
1362
+ suggested_role: step.role_priorities[0] || null,
1363
+ confidence: 'high',
1364
+ reason: `Step comes from selected workflow "${selectedWorkflowTemplate.name}".`,
1365
+ })),
1366
+ }
1367
+ : explicitCandidatePlan;
1368
+ if (candidateWorkflowPlan) {
1369
+ this.reportCandidateWorkflowProgress(opts, candidateWorkflowPlan);
1370
+ }
1371
+ const orchestratorActor = this.getOrchestratorDispatchActor(orchestratorBot);
1372
+ const contextWindow = conversationId
1373
+ ? (0, context_window_1.getPromptContextWindow)(conversationId, 5)
1374
+ : { summary: null, turns: [], carriedForward: false };
1375
+ const recentTurnsText = contextWindow.turns.length > 0
1376
+ ? (0, context_window_1.formatTurnsForPrompt)(contextWindow.turns)
1377
+ : '';
1378
+ const recentContext = conversationId
1379
+ ? this.buildRecentConversationContextForWorker(conversationId)
1380
+ : '';
1381
+ const isCliDispatchOrchestrator = !!(orchestratorBot && index_1.CLI_PROVIDERS.has(orchestratorBot.provider));
1382
+ const orchestratorCliSessionPlan = isCliDispatchOrchestrator
1383
+ ? (0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, orchestratorBot.id, orchestratorBot.provider)
1384
+ : null;
1385
+ const orchestratorBootstrapHistoryFilePath = isCliDispatchOrchestrator
1386
+ && !orchestratorCliSessionPlan?.resumeSessionId
1387
+ ? this.prepareBootstrapHistoryFile(conversationId)
1388
+ : null;
1389
+ const recentSummary = contextWindow.summary?.summary_text?.trim() || null;
1390
+ const shouldSendFreshCliFallbackContext = isCliDispatchOrchestrator
1391
+ && !orchestratorCliSessionPlan?.resumeSessionId
1392
+ && !orchestratorBootstrapHistoryFilePath;
1393
+ const dispatchRecentSummary = !isCliDispatchOrchestrator || shouldSendFreshCliFallbackContext
1394
+ ? recentSummary
1395
+ : null;
1396
+ const dispatchRecentTurns = !isCliDispatchOrchestrator || shouldSendFreshCliFallbackContext
1397
+ ? (recentTurnsText || null)
1398
+ : null;
1399
+ if (isCliDispatchOrchestrator && orchestratorBot) {
1400
+ const sessionLaunchReason = orchestratorCliSessionPlan?.resumeSessionId
1401
+ ? 'resumed'
1402
+ : ('mode' in contextWindow && contextWindow.mode === 'new_topic')
1403
+ ? 'new_topic/no_context'
1404
+ : 'fresh_with_bootstrap';
1405
+ console.info(`[orchestrator] session_launch_reason=${sessionLaunchReason} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} bootstrap=${!!orchestratorBootstrapHistoryFilePath}`);
1406
+ console.info(`[orchestrator] cli_session_selected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} selectedSessionId=${orchestratorCliSessionPlan?.resumeSessionId || '(none)'} resetReason=${orchestratorCliSessionPlan?.resetReason || '(none)'}`);
1407
+ if (orchestratorCliSessionPlan?.resumeSessionId) {
1408
+ console.info(`[orchestrator] cli_session_resuming conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} sessionId=${orchestratorCliSessionPlan.resumeSessionId}`);
1409
+ }
1410
+ else if ('mode' in contextWindow && contextWindow.mode === 'new_topic') {
1411
+ console.info(`[orchestrator] cli_session_fresh_without_bootstrap_new_topic conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
1412
+ }
1413
+ else if (orchestratorBootstrapHistoryFilePath) {
1414
+ console.info(`[orchestrator] cli_session_fresh_bootstrap_applied conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
1415
+ }
1416
+ else {
1417
+ console.warn(`[orchestrator] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} mode=${'mode' in contextWindow ? contextWindow.mode : 'unknown'}`);
1418
+ }
1419
+ }
1420
+ const precreatedCandidateTaskIds = candidateWorkflowPlan
1421
+ ? this.createPendingWorkflowProposal({
1422
+ prompt,
1423
+ conversationId,
1424
+ projectId: conversationProjectId || null,
1425
+ orchestratorActor,
1426
+ activeBots,
1427
+ selectedWorkflowTemplateHint,
1428
+ candidateWorkflowPlan,
1429
+ executeCreateWorkflow,
1430
+ recentContext,
1431
+ })
1432
+ : [];
1433
+ const existingTodoRows = data.listActiveTodoTasksForConversation(conversationId);
1434
+ const existingTodos = existingTodoRows.map((t) => ({
1435
+ id: t.id,
1436
+ owner_name: t.owner_name,
1437
+ title: t.title,
1438
+ // TodoTaskRow.status is a participant→boolean map ({"Ben":true,"John":false}).
1439
+ // Derive a human-readable status label for the hint block.
1440
+ status: this.summarizeTodoStatus(t.status),
1441
+ }));
1442
+ const turnCount = conversation?.turn_count || 0;
1443
+ // Read-side tools for the orchestrator. Steven's rule (april19fixes.txt
1444
+ // item 8): the orchestrator handles activities directly when no role-
1445
+ // specific bot applies. These tools let it read files, search the
1446
+ // codebase, look up TODOs, and fetch web content without delegating
1447
+ // trivial reads to worker bots. Write-side tools (write_file,
1448
+ // edit_file, run_command) are deliberately excluded — role
1449
+ // separation still sends those to workers via delegate_single.
1450
+ const { readFileTool } = await Promise.resolve().then(() => __importStar(require('./tools/read-file')));
1451
+ const { listDirectoryTool } = await Promise.resolve().then(() => __importStar(require('./tools/list-directory')));
1452
+ const { searchCodebaseTool } = await Promise.resolve().then(() => __importStar(require('./tools/search-codebase')));
1453
+ const { searchLocalMemoryTool } = await Promise.resolve().then(() => __importStar(require('./tools/search-local-memory')));
1454
+ const { webSearchTool } = await Promise.resolve().then(() => __importStar(require('./tools/web-search')));
1455
+ const { webFetchTool } = await Promise.resolve().then(() => __importStar(require('./tools/web-fetch')));
1456
+ const { listTasksTool } = await Promise.resolve().then(() => __importStar(require('./tools/todo-tasks')));
1457
+ const orchestratorTools = [
1458
+ readFileTool,
1459
+ listDirectoryTool,
1460
+ searchCodebaseTool,
1461
+ searchLocalMemoryTool,
1462
+ webSearchTool,
1463
+ webFetchTool,
1464
+ listTasksTool,
1465
+ ];
1466
+ const workspacePath = project?.folder?.trim() || opts.projectDir;
1467
+ const orchestratorToolContext = (0, index_2.createToolContext)(workspacePath, {
1468
+ runtimeMode: 'local_desktop',
1469
+ projectId: conversationProjectId || null,
1470
+ actorType: 'orchestrator',
1471
+ actorId: orchestratorBot?.id || orchestratorActor.actorId,
1472
+ botName: orchestratorActor.name,
1473
+ llmProvider: this.orchestratorRuntime.providerName,
1474
+ llmModel: this.orchestratorRuntime.model || undefined,
1475
+ });
1476
+ const result = await runDispatchTurn({
1477
+ userPrompt: prompt,
1478
+ orchestratorBot,
1479
+ orchestratorActor,
1480
+ llm,
1481
+ conversationId,
1482
+ projectId: conversationProjectId || null,
1483
+ conversationTitle: conversation?.title || null,
1484
+ topicId: null, // topic_id is not directly on conversation; callers that need it can extend later
1485
+ projectName: project?.name || null,
1486
+ projectFolder: project?.folder || null,
1487
+ turnCount,
1488
+ recentSummary: dispatchRecentSummary,
1489
+ recentTurnsForHint: dispatchRecentTurns,
1490
+ bootstrapHistoryFilePath: orchestratorBootstrapHistoryFilePath,
1491
+ recentContext,
1492
+ activeBots,
1493
+ selectedWorkflowTemplate: selectedWorkflowTemplateHint,
1494
+ candidateWorkflowPlan,
1495
+ precreatedCandidateTaskIds,
1496
+ existingTodos,
1497
+ effectivePolicyText,
1498
+ additionalGuidance: orchestratorBot?.soul_md || null,
1499
+ orchestratorTools,
1500
+ toolContext: orchestratorToolContext,
1501
+ });
1502
+ // Record dispatch outcome in the orchestration audit trail.
1503
+ try {
1504
+ orchestrationState.orchestratorBotId = orchestratorBot?.id || null;
1505
+ this.recordOrchestrationAudit(orchestrationState, 'choose_path', result.outcome.kind === 'error' ? 'blocked' : 'completed', `Tool-dispatch decision: ${result.outcome.kind}`, { outcomeKind: result.outcome.kind });
1506
+ }
1507
+ catch {
1508
+ // audit best-effort
1509
+ }
1510
+ this.setLastResponseMeta({
1511
+ agentName: 'Orchestrator',
1512
+ botId: orchestratorBot?.id || null,
1513
+ modelLabel: this.orchestratorRuntime.modelLabel || orchestratorBot?.model || null,
1514
+ });
1515
+ // reply_directly and error cases have no worker chain to run — return
1516
+ // immediately with the LLM's (or error) text.
1517
+ if (result.outcome.kind === 'reply_directly' || result.outcome.kind === 'error') {
1518
+ return result.userReplyText;
1519
+ }
1520
+ // delegate_single / create_workflow created TODOs. Fix for Codex QA
1521
+ // 2026-04-18 finding #1: the new path must EXECUTE the queued chain,
1522
+ // not just return a "queued" summary. Reuse the existing
1523
+ // dispatchQueuedTodoChain which walks active TODOs for this conversation
1524
+ // and runs each through workflowEngine.execute(). Without this, TODOs
1525
+ // would be orphaned in the queue with no worker ever picking them up.
1526
+ const createdTaskIds = [];
1527
+ if (result.outcome.kind === 'delegate_single') {
1528
+ const id = Number(result.outcome.taskId);
1529
+ if (Number.isFinite(id))
1530
+ createdTaskIds.push(id);
1531
+ }
1532
+ else if (result.outcome.kind === 'create_workflow') {
1533
+ for (const tid of result.outcome.taskIds) {
1534
+ const id = Number(tid);
1535
+ if (Number.isFinite(id))
1536
+ createdTaskIds.push(id);
1537
+ }
1538
+ }
1539
+ if (createdTaskIds.length > 0) {
1540
+ this.markConversationOrchestrated(conversationId);
1541
+ }
1542
+ if (opts.autoDispatchTodos === false) {
1543
+ const replyText = selectedWorkflowTemplate && result.outcome.kind === 'create_workflow'
1544
+ ? `Queued workflow from "${selectedWorkflowTemplate.name}".\n${result.userReplyText}`
1545
+ : result.userReplyText;
1546
+ orchestrationState.finalResponseDraft = replyText;
1547
+ return replyText;
1548
+ }
1549
+ return await this.dispatchQueuedTodoChain(conversationId, opts, orchestrationState, orchestratorActor, initialProject, {
1550
+ taskIds: createdTaskIds,
1551
+ workflowName: result.outcome.kind === 'create_workflow' ? (selectedWorkflowTemplate?.name || 'tool_dispatch_workflow') : null,
1552
+ });
1553
+ }
1554
+ async importPlanAsTodos(input) {
1555
+ const { executeCreateWorkflow } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-executor')));
1556
+ const { buildDispatchValidationContext, validateCreateWorkflow } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-tools')));
1557
+ const project = data.getProject(input.projectId);
1558
+ const activeBots = this.listProjectScopedBots(input.projectId || undefined, {
1559
+ includeOrchestratorProfiles: true,
1560
+ }).filter((bot) => bot.is_active === 1);
1561
+ if (activeBots.length === 0) {
1562
+ throw new Error('No active bots are assigned to this project.');
1563
+ }
1564
+ const selectedStages = (input.stages || [])
1565
+ .map((stage) => ({
1566
+ role: String(stage?.role || '').trim(),
1567
+ botId: String(stage?.botId || '').trim(),
1568
+ }))
1569
+ .filter((stage) => stage.role && stage.botId);
1570
+ if (selectedStages.length === 0) {
1571
+ throw new Error('Choose at least one stage and bot for the imported workflow.');
1572
+ }
1573
+ const plannerBot = this.resolvePlanImportBot(input.plannerBotId, input.projectId, input.orchestratorBot || null);
1574
+ const plannerRuntime = plannerBot && input.orchestratorBot && plannerBot.id === input.orchestratorBot.id
1575
+ ? this.orchestratorRuntime
1576
+ : buildLocalDesktopRuntimeFromProfile(plannerBot);
1577
+ const effectiveExecutionOrder = input.executionOrder === 'batch_by_stage' || input.executionOrder === 'per_item_pipeline'
1578
+ ? input.executionOrder
1579
+ : (0, plan_import_1.detectExecutionOrder)(input.plannerInstructions || null);
1580
+ const stageHints = selectedStages.map((stage) => {
1581
+ const stageBot = activeBots.find((bot) => bot.id === stage.botId);
1582
+ if (!stageBot) {
1583
+ throw new Error(`Plan import stage bot is not active in this project: ${stage.botId}`);
1584
+ }
1585
+ return {
1586
+ role: stage.role,
1587
+ botName: stageBot.name,
1588
+ };
1589
+ });
1590
+ const promptInput = {
1591
+ filePath: input.filePath,
1592
+ fileContent: input.fileContent,
1593
+ mode: input.mode,
1594
+ stages: stageHints,
1595
+ plannerInstructions: input.plannerInstructions || null,
1596
+ executionOrder: effectiveExecutionOrder,
1597
+ availableBots: activeBots.map((bot) => ({
1598
+ name: bot.name,
1599
+ provider: bot.provider,
1600
+ model: bot.model || null,
1601
+ rolePriorities: data.getAgentRolePriorities(bot),
1602
+ })),
1603
+ projectName: project?.name || null,
1604
+ projectFolder: project?.folder || input.opts.projectDir,
1605
+ };
1606
+ const systemPrompt = (0, plan_import_1.buildPlanImportSystemPrompt)(promptInput);
1607
+ const messages = [
1608
+ { role: 'user', content: (0, plan_import_1.buildPlanImportUserPrompt)(promptInput) },
1609
+ ];
1610
+ const validationContext = buildDispatchValidationContext(activeBots);
1611
+ let workflowArgs = null;
1612
+ let lastError = '';
1613
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1614
+ const rawContent = await this.runPlanImportPlannerTurn({
1615
+ plannerRuntime,
1616
+ plannerBot,
1617
+ conversationId: input.conversationId,
1618
+ projectId: input.projectId,
1619
+ workspacePath: project?.folder || input.opts.projectDir,
1620
+ systemPrompt,
1621
+ messages,
1622
+ abortSignal: input.opts.abortSignal,
1623
+ });
1624
+ const parsed = (0, plan_import_1.extractPlanImportResponse)(rawContent);
1625
+ if (parsed.error) {
1626
+ if (parsed.error === 'Planner authentication failed.') {
1627
+ lastError = `${plannerRuntime.agentName} could not authenticate. Check ${plannerRuntime.agentName}'s connection in Settings and retry.`;
1628
+ }
1629
+ else {
1630
+ lastError = parsed.error;
1631
+ }
1632
+ }
1633
+ else if (parsed.workflow) {
1634
+ const validation = validateCreateWorkflow(parsed.workflow, validationContext);
1635
+ if (validation.ok) {
1636
+ const detailValidation = (0, plan_import_1.validatePlanImportWorkflow)(parsed.workflow);
1637
+ if (!detailValidation.ok) {
1638
+ lastError = detailValidation.error || 'Imported workflow steps were too sparse.';
1639
+ }
1640
+ else if (effectiveExecutionOrder === 'batch_by_stage') {
1641
+ const orderValidation = (0, plan_import_1.validateBatchByStageOrdering)(parsed.workflow, stageHints);
1642
+ if (!orderValidation.ok) {
1643
+ lastError = orderValidation.error || 'Planner output interleaves stages instead of batching them.';
1644
+ }
1645
+ else {
1646
+ workflowArgs = parsed.workflow;
1647
+ break;
1648
+ }
1649
+ }
1650
+ else {
1651
+ workflowArgs = parsed.workflow;
1652
+ break;
1653
+ }
1654
+ }
1655
+ else {
1656
+ lastError = validation.error;
1657
+ }
1658
+ }
1659
+ else {
1660
+ lastError = 'Planner did not return a workflow.';
1661
+ }
1662
+ const plannerSnippet = rawContent
1663
+ .replace(/\s+/g, ' ')
1664
+ .slice(0, 300);
1665
+ console.warn(`[plan-import] invalid planner response attempt=${attempt + 1}/2 planner=${plannerRuntime.agentName} mode=${input.mode} error=${lastError} snippet="${plannerSnippet}"`);
1666
+ if (parsed.error === 'Planner authentication failed.') {
1667
+ break;
1668
+ }
1669
+ messages.push({ role: 'assistant', content: rawContent });
1670
+ messages.push({
1671
+ role: 'user',
1672
+ content: `The previous output was invalid: ${lastError}\nReturn corrected workflow step blocks only. Do not add prose before or after the workflow. Fully materialize every task with Files:, Do:/Verify:, and success criteria.`,
1673
+ });
1674
+ }
1675
+ if (!workflowArgs) {
1676
+ throw new Error(lastError || 'Planner could not produce a valid workflow import.');
1677
+ }
1678
+ const orchestratorActor = this.getOrchestratorDispatchActor(input.orchestratorBot || null);
1679
+ const existingTodoRows = data.listActiveTodoTasksForConversation(input.conversationId);
1680
+ const executionContext = {
1681
+ conversationId: input.conversationId,
1682
+ projectId: input.projectId,
1683
+ orchestratorActor,
1684
+ resolution: {
1685
+ activeBots,
1686
+ botsWithActiveTodos: new Set(existingTodoRows.map((row) => row.owner_name).filter(Boolean)),
1687
+ },
1688
+ userPrompt: `Imported plan from ${path.basename(input.filePath)} using ${plannerRuntime.agentName}.`,
1689
+ recentContext: null,
1690
+ workflowSourceLabel: `Imported plan ${path.basename(input.filePath)}`,
1691
+ };
1692
+ const result = executeCreateWorkflow(workflowArgs, executionContext);
1693
+ if (result.kind === 'error') {
1694
+ throw new Error(result.error);
1695
+ }
1696
+ const taskIds = result.tasks.map((task) => Number(task.id)).filter((id) => Number.isFinite(id));
1697
+ const ownerNames = Array.from(new Set(result.tasks.map((task) => task.owner_name).filter(Boolean)));
1698
+ const planRun = data.createImportedPlanRun({
1699
+ conversationId: input.conversationId,
1700
+ projectId: input.projectId,
1701
+ sourceFilePath: input.filePath,
1702
+ sourceFileName: path.basename(input.filePath),
1703
+ plannerBotId: plannerBot.id,
1704
+ plannerAgentName: plannerRuntime.agentName,
1705
+ plannerProvider: plannerRuntime.providerName,
1706
+ plannerModel: plannerRuntime.model,
1707
+ requestedStages: selectedStages,
1708
+ requestedOptions: {
1709
+ plannerInstructions: input.plannerInstructions || null,
1710
+ executionOrder: effectiveExecutionOrder,
1711
+ },
1712
+ taskIds,
1713
+ status: input.autoDispatchTodos ? 'running' : 'paused',
1714
+ });
1715
+ if (input.autoDispatchTodos) {
1716
+ const state = (0, state_1.createOrchestrationState)({
1717
+ conversationId: input.conversationId,
1718
+ projectId: input.projectId,
1719
+ orchestratorBotId: input.orchestratorBot?.id || this.orchestratorRuntime.botId || null,
1720
+ userPromptRaw: `Imported plan ${path.basename(input.filePath)}`,
1721
+ });
1722
+ state.selectedPath = 'delegate';
1723
+ await this.dispatchQueuedTodoChain(input.conversationId, input.opts, state, orchestratorActor, project, {
1724
+ taskIds,
1725
+ workflowName: path.basename(input.filePath),
1726
+ });
1727
+ }
1728
+ const fileLabel = path.basename(input.filePath);
1729
+ const pausedText = input.autoDispatchTodos ? 'and started the workflow' : 'and left the workflow paused for review';
1730
+ return {
1731
+ planRunId: planRun.id,
1732
+ replyText: `Imported ${fileLabel} into ${result.tasks.length} TODOs ${pausedText}. Planner: ${plannerRuntime.agentName}${plannerRuntime.model ? ` (${plannerRuntime.model})` : ''}.`,
1733
+ taskIds,
1734
+ ownerNames,
1735
+ planner: {
1736
+ botId: plannerBot.id,
1737
+ provider: plannerRuntime.providerName,
1738
+ model: plannerRuntime.model,
1739
+ agentName: plannerRuntime.agentName,
1740
+ },
1741
+ };
1742
+ }
1743
+ async startImportedPlanRun(input) {
1744
+ const run = data.getImportedPlanRun(input.runId);
1745
+ if (!run) {
1746
+ throw new Error(`Imported plan run not found: ${input.runId}`);
1747
+ }
1748
+ if (run.status === 'discarded') {
1749
+ throw new Error('This imported plan was discarded and cannot be started.');
1750
+ }
1751
+ const project = run.project_id ? data.getProject(run.project_id) : undefined;
1752
+ const remainingTaskIds = data.getImportedPlanRunRemainingTaskIds(run.id);
1753
+ if (remainingTaskIds.length === 0) {
1754
+ const completedRun = data.updateImportedPlanRun(run.id, {
1755
+ status: 'completed',
1756
+ completedAt: run.completed_at || new Date().toISOString(),
1757
+ lastError: null,
1758
+ }) || run;
1759
+ return {
1760
+ run: completedRun,
1761
+ replyText: `Imported plan ${run.source_file_name} is already complete.`,
1762
+ remainingTaskIds: [],
1763
+ completed: true,
1764
+ };
1765
+ }
1766
+ const orchestratorActor = this.getOrchestratorDispatchActor(input.orchestratorBot || null);
1767
+ const state = (0, state_1.createOrchestrationState)({
1768
+ conversationId: run.conversation_id,
1769
+ projectId: run.project_id || null,
1770
+ orchestratorBotId: input.orchestratorBot?.id || this.orchestratorRuntime.botId || null,
1771
+ userPromptRaw: `Resume imported plan ${run.source_file_name}`,
1772
+ });
1773
+ state.selectedPath = 'delegate';
1774
+ data.updateImportedPlanRun(run.id, {
1775
+ status: 'running',
1776
+ startedAt: run.started_at || new Date().toISOString(),
1777
+ completedAt: null,
1778
+ discardedAt: null,
1779
+ lastError: null,
1780
+ });
1781
+ let replyText = '';
1782
+ try {
1783
+ replyText = await this.dispatchQueuedTodoChain(run.conversation_id, input.opts, state, orchestratorActor, project, {
1784
+ taskIds: remainingTaskIds,
1785
+ workflowName: run.source_file_name,
1786
+ });
1787
+ }
1788
+ catch (err) {
1789
+ const pausedRun = data.updateImportedPlanRun(run.id, {
1790
+ status: 'paused',
1791
+ lastError: err?.message || 'Imported plan execution failed.',
1792
+ }) || run;
1793
+ return {
1794
+ run: pausedRun,
1795
+ replyText: err?.message || 'Imported plan execution failed.',
1796
+ remainingTaskIds: data.getImportedPlanRunRemainingTaskIds(run.id),
1797
+ completed: false,
1798
+ };
1799
+ }
1800
+ const remainingAfter = data.getImportedPlanRunRemainingTaskIds(run.id);
1801
+ const completed = remainingAfter.length === 0;
1802
+ const updatedRun = data.updateImportedPlanRun(run.id, {
1803
+ status: completed ? 'completed' : 'paused',
1804
+ completedAt: completed ? new Date().toISOString() : null,
1805
+ lastError: completed ? null : (replyText || state.finalResponseDraft || null),
1806
+ }) || run;
1807
+ return {
1808
+ run: updatedRun,
1809
+ replyText,
1810
+ remainingTaskIds: remainingAfter,
1811
+ completed,
1812
+ };
1813
+ }
1814
+ buildDirectExecutionSystemPrompt(prompt, conversationId, project) {
1815
+ const contextWindow = conversationId
1816
+ ? (0, context_window_1.getPromptContextWindow)(conversationId, 5)
1817
+ : { summary: null, turns: [], carriedForward: false };
1818
+ const recentTurnsText = (0, context_window_1.formatTurnsForPrompt)(contextWindow.turns) || '(none)';
1819
+ const recentSummaryText = contextWindow.summary?.summary_text?.trim()
1820
+ ? `${contextWindow.carriedForward ? '(carried forward from the previous conversation in this topic)\n' : ''}${contextWindow.summary.summary_text.trim()}`
1821
+ : '(none)';
1822
+ return [
1823
+ 'You are the user-facing Orchestrator in Funolio local desktop mode.',
1824
+ 'You are handling this request directly.',
1825
+ 'Use the available tools whenever they are helpful.',
1826
+ 'Do not delegate, do not create workflow TODOs, and do not claim another bot is doing the work.',
1827
+ 'If the user asked to read, inspect, summarize, or show a file, do that directly.',
1828
+ 'If the user asked to update plain-text notes, plans, summaries, or TODO-like content, do that directly.',
1829
+ 'If you cannot complete the request directly, explain the concrete blocker briefly.',
1830
+ '',
1831
+ '[Project Context]',
1832
+ `Project: ${project?.name || '(none)'}`,
1833
+ `Workspace: ${project?.folder || '(none)'}`,
1834
+ '',
1835
+ '[Rolling Summary]',
1836
+ recentSummaryText,
1837
+ '',
1838
+ '[Recent Turns]',
1839
+ recentTurnsText,
1840
+ '',
1841
+ '[Current Request]',
1842
+ prompt,
1843
+ ].join('\n');
1844
+ }
1845
+ async executeClerkOwnedWorkDirect(prompt, conversationId, orchestratorBot, project, opts) {
1846
+ const systemPrompt = this.buildDirectExecutionSystemPrompt(prompt, conversationId, project);
1847
+ const workspacePath = project?.folder?.trim() || opts.projectDir;
1848
+ const toolContext = (0, index_2.createToolContext)(workspacePath, {
1849
+ runtimeMode: 'local_desktop',
1850
+ projectId: project?.id || opts.projectId || null,
1851
+ actorType: 'orchestrator',
1852
+ actorId: 'Orchestrator',
1853
+ botName: 'Orchestrator',
1854
+ llmProvider: this.orchestratorRuntime.providerName,
1855
+ llmModel: this.orchestratorRuntime.model || undefined,
1856
+ });
1857
+ const toolDefinitions = (0, index_2.getAllToolDefinitions)('local_desktop');
1858
+ const messages = [{ role: 'user', content: prompt }];
1859
+ const permissionMode = (orchestratorBot.permission_mode || 'autopilot');
1860
+ for (let iteration = 0; iteration < 8; iteration++) {
1861
+ const response = await this.orchestratorRuntime.llm.chat({
1862
+ messages,
1863
+ system: systemPrompt,
1864
+ stream: true,
1865
+ tools: toolDefinitions,
1866
+ toolChoice: 'auto',
1867
+ runtimeMode: 'local_desktop',
1868
+ onChunk: opts.onWorkerChunk
1869
+ ? async (chunk) => {
1870
+ if (!chunk)
1871
+ return;
1872
+ opts.onWorkerChunk?.({
1873
+ type: 'chunk',
1874
+ text: chunk,
1875
+ stepId: 'orchestrator-direct',
1876
+ agentName: 'Orchestrator',
1877
+ description: 'Direct orchestrator work',
1878
+ stepIndex: 0,
1879
+ totalSteps: 1,
1880
+ });
1881
+ }
1882
+ : undefined,
1883
+ });
1884
+ if (response.toolCalls?.length) {
1885
+ messages.push({
1886
+ role: 'assistant',
1887
+ content: response.content || '',
1888
+ toolCalls: response.toolCalls,
1889
+ });
1890
+ for (const tc of response.toolCalls) {
1891
+ const approval = (0, approval_1.checkPermission)(tc.name, permissionMode);
1892
+ if (!approval.approved) {
1893
+ throw new Error(`APPROVAL_REQUIRED: ${approval.reason}`);
1894
+ }
1895
+ opts.onWorkerChunk?.({
1896
+ type: 'tool_call',
1897
+ text: '',
1898
+ stepId: 'orchestrator-direct',
1899
+ agentName: 'Orchestrator',
1900
+ description: 'Direct orchestrator work',
1901
+ stepIndex: 0,
1902
+ totalSteps: 1,
1903
+ toolCallId: tc.id,
1904
+ toolName: tc.name,
1905
+ toolArguments: tc.arguments,
1906
+ });
1907
+ const result = await (0, index_2.executeAndVerify)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolContext);
1908
+ const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown'}`;
1909
+ messages.push({
1910
+ role: 'tool',
1911
+ content: output,
1912
+ toolCallId: tc.id,
1913
+ toolName: tc.name,
1914
+ });
1915
+ opts.onWorkerChunk?.({
1916
+ type: 'tool_result',
1917
+ text: '',
1918
+ stepId: 'orchestrator-direct',
1919
+ agentName: 'Orchestrator',
1920
+ description: 'Direct orchestrator work',
1921
+ stepIndex: 0,
1922
+ totalSteps: 1,
1923
+ toolCallId: tc.id,
1924
+ toolName: tc.name,
1925
+ toolOutput: output.length > 500 ? `${output.slice(0, 500)}...` : output,
1926
+ toolIsError: !result.success,
1927
+ });
1928
+ }
1929
+ continue;
1930
+ }
1931
+ return String(response.content || '').trim();
1932
+ }
1933
+ throw new Error('Orchestrator direct execution exceeded the maximum tool iterations.');
1536
1934
  }
1537
1935
  async handleOrchestratorFrontDoor(prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state) {
1538
1936
  (0, state_1.addHelperRoleUsage)(state, 'orchestrator_front_door');
@@ -1543,19 +1941,30 @@ class OrchestratorAgent {
1543
1941
  const recentSummaryText = contextWindow.summary?.summary_text?.trim()
1544
1942
  ? `${contextWindow.carriedForward ? '(carried forward from the previous conversation in this topic)\n' : ''}${contextWindow.summary.summary_text.trim()}`
1545
1943
  : '(none)';
1546
- // Build specialists list from non-orchestrator agent profiles
1944
+ // Build specialists list include all bots except pure orchestrators.
1945
+ // A bot that is_orchestrator but also has a worker role_class (code, qa, research, etc.)
1946
+ // should still appear as an available worker.
1547
1947
  const allProfiles = data.listAgentProfiles();
1548
- const specialists = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(allProfiles)
1948
+ const PURE_ORCHESTRATOR_ROLES = new Set(['orchestrator', 'project manager']);
1949
+ const specialists = allProfiles
1950
+ .filter((agent) => {
1951
+ const rc = this.getOrchestrationRoleClass(agent);
1952
+ if (agent.is_orchestrator === 1 && PURE_ORCHESTRATOR_ROLES.has(rc))
1953
+ return false;
1954
+ if (!agent.is_orchestrator && (0, orchestrator_profile_1.isOrchestratorProfile)(agent))
1955
+ return false;
1956
+ return true;
1957
+ })
1549
1958
  .map((agent) => ({
1550
1959
  name: agent.name,
1551
- roleLabel: agent.role_label || agent.role_class || 'general',
1960
+ roleLabel: this.getOrchestrationRoleLabel(agent),
1552
1961
  references: this.getAgentRoutingReferences(agent),
1553
1962
  responsibilities: this.describeAgentResponsibilities(agent),
1554
1963
  }));
1555
1964
  const signalSpecialists = allProfiles
1556
1965
  .map((agent) => ({
1557
1966
  name: agent.name,
1558
- roleLabel: agent.role_label || agent.role_class || 'general',
1967
+ roleLabel: this.getOrchestrationRoleLabel(agent),
1559
1968
  references: this.getAgentRoutingReferences(agent),
1560
1969
  responsibilities: this.describeAgentResponsibilities(agent),
1561
1970
  }));
@@ -1586,12 +1995,56 @@ class OrchestratorAgent {
1586
1995
  fastPath: false,
1587
1996
  });
1588
1997
  }
1589
- const useSinglePassOrchestrator = this.isLocalDesktopRuntime();
1998
+ const useSinglePassOrchestrator = this.isLocalDesktopRuntime() && this.orchestratorRuntime.kind === 'bot';
1999
+ const orchestratorCliSessionPlan = useSinglePassOrchestrator
2000
+ && conversationId
2001
+ && index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
2002
+ ? (0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, orchestratorBot.id, orchestratorBot.provider)
2003
+ : null;
2004
+ const orchestratorBootstrapHistoryFilePath = this.isLocalDesktopRuntime()
2005
+ && conversationId
2006
+ && index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
2007
+ && !orchestratorCliSessionPlan?.resumeSessionId
2008
+ ? this.prepareBootstrapHistoryFile(conversationId)
2009
+ : null;
2010
+ const localPromptContract = useSinglePassOrchestrator
2011
+ ? (index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
2012
+ && conversationId
2013
+ && !!orchestratorCliSessionPlan?.resumeSessionId
2014
+ ? 'cli_recurring'
2015
+ : 'api_or_fresh_cli')
2016
+ : 'legacy';
2017
+ if (useSinglePassOrchestrator && index_1.CLI_PROVIDERS.has(orchestratorBot.provider)) {
2018
+ const sessionLaunchReason = orchestratorCliSessionPlan?.resumeSessionId
2019
+ ? 'resumed'
2020
+ : ('mode' in contextWindow && contextWindow.mode === 'new_topic')
2021
+ ? 'new_topic/no_context'
2022
+ : 'fresh_with_bootstrap';
2023
+ console.info(`[orchestrator-front-door] session_launch_reason=${sessionLaunchReason} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} bootstrap=${!!orchestratorBootstrapHistoryFilePath}`);
2024
+ console.info(`[orchestrator-front-door] cli_session_selected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} selectedSessionId=${orchestratorCliSessionPlan?.resumeSessionId || '(none)'} resetReason=${orchestratorCliSessionPlan?.resetReason || '(none)'}`);
2025
+ if (orchestratorCliSessionPlan?.resumeSessionId) {
2026
+ console.info(`[orchestrator-front-door] cli_session_resuming conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} sessionId=${orchestratorCliSessionPlan.resumeSessionId}`);
2027
+ }
2028
+ else if ('mode' in contextWindow && contextWindow.mode === 'new_topic') {
2029
+ console.info(`[orchestrator-front-door] cli_session_fresh_without_bootstrap_new_topic conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
2030
+ }
2031
+ else if (orchestratorBootstrapHistoryFilePath) {
2032
+ console.info(`[orchestrator-front-door] cli_session_fresh_bootstrap_applied conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
2033
+ }
2034
+ else {
2035
+ console.warn(`[orchestrator-front-door] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} mode=${'mode' in contextWindow ? contextWindow.mode : 'unknown'}`);
2036
+ }
2037
+ }
2038
+ const isClerkRuntime = this.orchestratorRuntime.kind === 'clerk';
2039
+ const orchestratorDisplayName = isClerkRuntime && !orchestratorBot.is_orchestrator
2040
+ ? 'Orchestrator'
2041
+ : orchestratorBot.name;
1590
2042
  const operatingPrompt = (0, orchestrator_operating_prompt_1.buildOrchestratorOperatingPrompt)({
1591
- orchestratorName: orchestratorBot.name,
1592
- primaryRole: orchestratorBot.role_label || orchestratorBot.role_class || null,
2043
+ orchestratorName: orchestratorDisplayName,
2044
+ primaryRole: isClerkRuntime ? 'project manager' : data.getAgentOrchestrationRoleLabel(orchestratorBot),
1593
2045
  specialists,
1594
2046
  workflowNames,
2047
+ localPromptContract,
1595
2048
  entryMode: frontDoorSignals.explicitOrchestrationRequested ? 'orchestration_bias' : 'normal',
1596
2049
  signalSummary: {
1597
2050
  matchedBots: frontDoorSignals.matchedBots,
@@ -1610,7 +2063,9 @@ class OrchestratorAgent {
1610
2063
  }),
1611
2064
  recentSummary: recentSummaryText,
1612
2065
  lastFiveTurns: recentTurnsText,
2066
+ bootstrapHistoryFilePath: orchestratorBootstrapHistoryFilePath,
1613
2067
  decisionOnly: !useSinglePassOrchestrator,
2068
+ defaultBotName: data.getDefaultAgentProfile()?.name || roleAssignments.coding || null,
1614
2069
  });
1615
2070
  const decisionPrompt = [
1616
2071
  'This is a routing decision only. Do not perform the task.',
@@ -1621,20 +2076,21 @@ class OrchestratorAgent {
1621
2076
  'Return strict JSON only.',
1622
2077
  ].join('\n');
1623
2078
  let decision = null;
1624
- this.reportHiddenRoleProgress(opts, 'orchestrator', 'Understanding request');
1625
- opts.onProgress('Understanding request...');
1626
- let understandingHeartbeat = setInterval(() => {
1627
- this.reportHiddenRoleProgress(opts, 'orchestrator', 'Still understanding the request');
1628
- }, 8_000);
2079
+ // No robotic 'Understanding request' progress message — the streamed
2080
+ // narration from the LLM front-door call is what the user sees instead.
2081
+ // Heartbeat removed for the same reason.
2082
+ let understandingHeartbeat = setInterval(() => { }, 1_000_000);
1629
2083
  try {
1630
2084
  if (useSinglePassOrchestrator) {
1631
2085
  const result = await this.runNodeWithRetry('understand_request', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
2086
+ abortSignal: opts.abortSignal,
1632
2087
  disableDecomposition: true,
1633
2088
  isOrchestrated: false,
1634
2089
  projectId: projectId || null,
1635
2090
  workspacePath: project?.folder?.trim() || undefined,
1636
2091
  persistConversationMessages: false,
1637
2092
  workerMode: false,
2093
+ storedAttachments: opts.storedAttachments,
1638
2094
  systemPromptOverride: operatingPrompt,
1639
2095
  onWorkerChunk: opts.onWorkerChunk,
1640
2096
  onProgress: (progress) => {
@@ -1656,14 +2112,16 @@ class OrchestratorAgent {
1656
2112
  }
1657
2113
  }
1658
2114
  else {
1659
- const result = await this.runNodeWithRetry('understand_request', state, async () => this.workflowEngine.execute(decisionPrompt, null, orchestratorBot.id, {
1660
- disableDecomposition: true,
1661
- disableTools: true,
2115
+ const rawDecision = await this.runNodeWithRetry('understand_request', state, async () => this.runOrchestratorPrompt(decisionPrompt, operatingPrompt, {
2116
+ conversationId: null,
2117
+ orchestratorBot,
1662
2118
  projectId: projectId || null,
1663
2119
  workspacePath: project?.folder?.trim() || undefined,
1664
- systemPromptOverride: operatingPrompt,
2120
+ disableTools: true,
2121
+ abortSignal: opts.abortSignal,
2122
+ onNarrationChunk: this.makeOrchestratorNarrationForwarder(opts, orchestratorBot),
1665
2123
  }));
1666
- decision = (0, orchestrator_operating_prompt_1.parseOrchestratorFrontDoorDecision)(result.mergedResult);
2124
+ decision = (0, orchestrator_operating_prompt_1.parseOrchestratorFrontDoorDecision)(rawDecision);
1667
2125
  }
1668
2126
  }
1669
2127
  catch {
@@ -1721,7 +2179,20 @@ class OrchestratorAgent {
1721
2179
  };
1722
2180
  }
1723
2181
  }
1724
- return this.handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, signalSpecialists, workflowNames);
2182
+ // Fallback narration: if the decision has no narration (LLM call failed
2183
+ // or returned a code-fallback decision), synthesize a short factual one
2184
+ // and emit it so the user sees something natural in the orchestrator card.
2185
+ // Pass roleAssignments so workflow fallbacks can use real worker names.
2186
+ if (decision && !decision.narration?.trim()) {
2187
+ const fallbackText = this.synthesizeFallbackNarration(decision, roleAssignments);
2188
+ if (fallbackText) {
2189
+ decision.narration = fallbackText;
2190
+ const forwarder = this.makeOrchestratorNarrationForwarder(opts, orchestratorBot);
2191
+ if (forwarder)
2192
+ forwarder(fallbackText);
2193
+ }
2194
+ }
2195
+ return this.handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, frontDoorSignals, signalSpecialists, workflowNames);
1725
2196
  }
1726
2197
  buildFrontDoorSystemSuggestion(prompt, frontDoorSignals, promptAssignments, roleAssignments) {
1727
2198
  const normalizeName = (value) => String(value || '').trim().toLowerCase();
@@ -1766,7 +2237,7 @@ class OrchestratorAgent {
1766
2237
  delegateRole,
1767
2238
  };
1768
2239
  }
1769
- async handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, signalSpecialists, workflowNames) {
2240
+ async handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, frontDoorSignals, signalSpecialists, workflowNames) {
1770
2241
  if (!decision) {
1771
2242
  this.recordOrchestrationAudit(state, 'understand_request', 'blocked', 'Orchestrator-first decision phase did not return valid JSON. Falling back to legacy clerk chain.', { featureFlag: 'orchestrator_v2_enabled' });
1772
2243
  return null;
@@ -1774,21 +2245,16 @@ class OrchestratorAgent {
1774
2245
  const highRiskPrompt = this.isHighRiskPrompt(prompt);
1775
2246
  state.riskLevel = highRiskPrompt ? 'high' : state.riskLevel;
1776
2247
  this.recordOrchestrationAudit(state, 'risk_gate', 'risk_assessed', highRiskPrompt ? 'Prompt matched high-risk action patterns.' : 'No high-risk action patterns matched before execution.', { highRiskPrompt });
1777
- const normalizedDecision = (0, front_door_policy_1.applyFrontDoorPolicy)({
1778
- prompt,
1779
- initialDecision: decision,
1780
- orchestratorName: orchestratorBot.name,
1781
- orchestratorRoleClass: orchestratorBot.role_class,
1782
- promptAssignments,
1783
- roleAssignments,
1784
- specialists: signalSpecialists,
1785
- workflowNames,
1786
- highRisk: highRiskPrompt,
1787
- });
1788
- if (normalizedDecision.corrected) {
1789
- (0, state_1.markMisrouteCorrected)(state);
1790
- }
1791
- decision = normalizedDecision.decision;
2248
+ const normalizedDecision = {
2249
+ decision,
2250
+ corrected: false,
2251
+ correctionReason: undefined,
2252
+ taskType: (0, front_door_policy_1.inferFrontDoorTaskType)(prompt),
2253
+ qaMode: 'none',
2254
+ preferredWorkflowRequested: frontDoorSignals.matchedWorkflowNames.length > 0,
2255
+ explicitWorkflowRequested: frontDoorSignals.explicitOrchestrationRequested,
2256
+ signals: frontDoorSignals,
2257
+ };
1792
2258
  state.intent = decision.mode;
1793
2259
  state.taskType = normalizedDecision.taskType;
1794
2260
  this.recordOrchestrationAudit(state, 'choose_path', 'path_selected', normalizedDecision.correctionReason || decision.reason || `Orchestrator selected ${decision.mode}.`, {
@@ -1831,11 +2297,39 @@ class OrchestratorAgent {
1831
2297
  return response;
1832
2298
  }
1833
2299
  if (decision.mode === 'execute_self') {
1834
- // No guardrails, no confirmation checkpoints, no validation blocking.
1835
- // Ben just does the work — like Claude CLI.
1836
2300
  (0, state_1.setOrchestrationPath)(state, 'direct', 'direct');
1837
2301
  const directResult = await this.executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state);
1838
- return directResult.response;
2302
+ if (directResult.ok) {
2303
+ return directResult.response;
2304
+ }
2305
+ const fallbackAgent = this.resolveDefaultFallbackAgent(project?.id || state.projectId || undefined, orchestratorBot, roleAssignments);
2306
+ if (!fallbackAgent) {
2307
+ return directResult.response;
2308
+ }
2309
+ const fallbackIntent = {
2310
+ primaryMode: 'PROXY_MODE',
2311
+ secondaryModes: [],
2312
+ executionOrder: ['PROXY_MODE'],
2313
+ userFacingMode: 'ASK_WORKER',
2314
+ targetScope: 'ONE_WORKER',
2315
+ confidence: 'MEDIUM',
2316
+ intent: normalizedDecision.taskType === 'qa'
2317
+ ? 'review'
2318
+ : normalizedDecision.taskType === 'research'
2319
+ ? 'brainstorm'
2320
+ : 'build',
2321
+ targetAgent: fallbackAgent.name,
2322
+ isMultiStep: false,
2323
+ reasoning: `${orchestratorBot.name} could not complete the kept task, so it is falling back to default bot ${fallbackAgent.name}.`,
2324
+ };
2325
+ const fallbackExecutionSpec = this.buildFrontDoorDelegateExecutionSpec(prompt, fallbackIntent, fallbackAgent.name);
2326
+ this.applyExecutionSpecToState(state, fallbackExecutionSpec);
2327
+ this.recordOrchestrationAudit(state, 'delegate_specialist', 'delegated', `${orchestratorBot.name} could not finish the kept task, so it is falling back to default bot ${fallbackAgent.name}.`, {
2328
+ targetAgentId: fallbackAgent.id,
2329
+ targetAgentName: fallbackAgent.name,
2330
+ fallbackReason: directResult.error || null,
2331
+ });
2332
+ return this.handleProxyRequest(prompt, conversationId, fallbackIntent, opts, fallbackExecutionSpec, roleAssignments, state);
1839
2333
  }
1840
2334
  if (decision.mode === 'delegate') {
1841
2335
  const delegateTarget = this.resolveDelegatedAgentName(decision, roleAssignments);
@@ -1877,7 +2371,7 @@ class OrchestratorAgent {
1877
2371
  if (executionSpec.requiresConfirmation) {
1878
2372
  return this.createConfirmationCheckpoint(delegatePrompt, conversationId, delegateIntent, executionSpec, projectId, roleAssignments, state);
1879
2373
  }
1880
- return await this.queueDelegateTodo(delegatePrompt, conversationId, delegateIntent, opts, executionSpec, roleAssignments, state, orchestratorBot, delegateTarget);
2374
+ return await this.queueDelegateTodo(delegatePrompt, conversationId, delegateIntent, opts, executionSpec, roleAssignments, state, this.getOrchestratorDispatchActor(orchestratorBot), delegateTarget);
1881
2375
  }
1882
2376
  if (decision.mode === 'workflow') {
1883
2377
  const workflowObjective = decision.workflow_request?.trim() || prompt;
@@ -1907,19 +2401,32 @@ class OrchestratorAgent {
1907
2401
  return this.createConfirmationCheckpoint(prompt, conversationId, workflowIntent, validation.executionSpec, projectId, roleAssignments, state);
1908
2402
  }
1909
2403
  this.recordOrchestrationAudit(state, 'run_workflow', 'delegated', 'Workflow execution is using the original user prompt as the execution source of truth.', { workflowObjective });
1910
- return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, roleAssignments, project, state, orchestratorBot);
2404
+ return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, roleAssignments, project, state, this.getOrchestratorDispatchActor(orchestratorBot));
1911
2405
  }
1912
2406
  return null;
1913
2407
  }
1914
2408
  resolveDelegatedAgentName(decision, roleAssignments) {
1915
2409
  const target = decision.delegate_target?.trim();
2410
+ // Validate delegate_target matches delegate_role — if the LLM says role=qa
2411
+ // but target=Ben (the default/coding bot), override with the correct role assignment.
2412
+ // This prevents the LLM from always defaulting to the "default bot" for every role.
2413
+ const role = decision.delegate_role;
2414
+ if (role && target && target.toLowerCase() !== 'none') {
2415
+ const roleBot = role === 'qa' ? roleAssignments.qa
2416
+ : role === 'research' ? roleAssignments.research
2417
+ : role === 'coding' ? roleAssignments.coding
2418
+ : undefined;
2419
+ if (roleBot && roleBot.toLowerCase() !== target.toLowerCase()) {
2420
+ return roleBot;
2421
+ }
2422
+ }
1916
2423
  if (target && target.toLowerCase() !== 'none')
1917
2424
  return target;
1918
- if (decision.delegate_role === 'coding')
2425
+ if (role === 'coding')
1919
2426
  return roleAssignments.coding;
1920
- if (decision.delegate_role === 'qa')
2427
+ if (role === 'qa')
1921
2428
  return roleAssignments.qa;
1922
- if (decision.delegate_role === 'research')
2429
+ if (role === 'research')
1923
2430
  return roleAssignments.research;
1924
2431
  return undefined;
1925
2432
  }
@@ -1983,7 +2490,7 @@ class OrchestratorAgent {
1983
2490
  locked: true,
1984
2491
  };
1985
2492
  }
1986
- async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state, orchestratorBot, delegateTarget) {
2493
+ async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state, orchestratorActor, delegateTarget) {
1987
2494
  const projectId = opts.projectId || state.projectId || null;
1988
2495
  const project = projectId ? data.getProject(projectId) : undefined;
1989
2496
  const targetAgent = this.findAgentByName(delegateTarget, project?.id || undefined);
@@ -1994,7 +2501,7 @@ class OrchestratorAgent {
1994
2501
  clarificationQuestions: [`I could not resolve the target bot "${delegateTarget}". Which worker should own this task?`],
1995
2502
  }, projectId ? data.getProjectOverview(projectId) : undefined);
1996
2503
  }
1997
- const step = this.buildSingleDelegateTodoStep(prompt, targetAgent, orchestratorBot, project?.name || null, project?.folder || null, data.getEffectiveOrchestrationPolicy(projectId || undefined));
2504
+ const step = this.buildSingleDelegateTodoStep(prompt, conversationId, targetAgent, orchestratorActor, project?.name || null, project?.folder || null, data.getEffectiveOrchestrationPolicy(projectId || undefined));
1998
2505
  const task = data.addTodoTask({
1999
2506
  projectId,
2000
2507
  conversationId,
@@ -2007,7 +2514,7 @@ class OrchestratorAgent {
2007
2514
  taskType: step.taskType,
2008
2515
  profileName: 'Programming',
2009
2516
  taskPrompt: step.taskPrompt,
2010
- actor: { actorType: 'orchestrator', actorId: orchestratorBot.id },
2517
+ actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
2011
2518
  });
2012
2519
  this.markConversationOrchestrated(conversationId);
2013
2520
  this.recordOrchestrationAudit(state, 'delegate_specialist', 'delegated', `Queued delegated TODO for ${targetAgent.name}.`, {
@@ -2027,14 +2534,14 @@ class OrchestratorAgent {
2027
2534
  ].join('\n');
2028
2535
  return state.finalResponseDraft;
2029
2536
  }
2030
- return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, { taskIds: [task.id], workflowName: null });
2537
+ return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, { taskIds: [task.id], workflowName: null });
2031
2538
  }
2032
- async queueWorkflowTodoPlan(prompt, conversationId, intent, executionSpec, opts, roleAssignments, project, state, orchestratorBot, workflowTemplate) {
2539
+ async queueWorkflowTodoPlan(prompt, conversationId, intent, executionSpec, opts, roleAssignments, project, state, orchestratorActor, workflowTemplate) {
2033
2540
  const projectId = project?.id || opts.projectId || state.projectId || null;
2034
2541
  const effectivePolicy = data.getEffectiveOrchestrationPolicy(projectId || undefined);
2035
2542
  const plannedSteps = workflowTemplate
2036
- ? this.buildTodoStepsFromTemplate(prompt, workflowTemplate, orchestratorBot, project?.name || null, project?.folder || null, effectivePolicy)
2037
- : this.buildTodoStepsFromAssignments(prompt, roleAssignments, intent, orchestratorBot, projectId || undefined, project?.name || null, project?.folder || null, effectivePolicy);
2543
+ ? this.buildTodoStepsFromTemplate(prompt, conversationId, workflowTemplate, orchestratorActor, project?.name || null, project?.folder || null, effectivePolicy)
2544
+ : this.buildTodoStepsFromAssignments(prompt, conversationId, roleAssignments, intent, orchestratorActor, projectId || undefined, project?.name || null, project?.folder || null, effectivePolicy);
2038
2545
  if (plannedSteps.length === 0) {
2039
2546
  const overview = projectId ? data.getProjectOverview(projectId) : undefined;
2040
2547
  return this.formatClarificationResponse({
@@ -2060,9 +2567,9 @@ class OrchestratorAgent {
2060
2567
  profileName: 'Programming',
2061
2568
  nextWorkerBotId: next?.owner.id || null,
2062
2569
  nextWorkerName: next?.owner.name || null,
2063
- nextWorkerRole: next ? (next.owner.role_label || next.owner.role_class || null) : null,
2570
+ nextWorkerRole: next ? data.getAgentOrchestrationRoleLabel(next.owner) : null,
2064
2571
  taskPrompt: step.taskPrompt,
2065
- actor: { actorType: 'orchestrator', actorId: orchestratorBot.id },
2572
+ actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
2066
2573
  });
2067
2574
  createdTasks.push(task);
2068
2575
  (0, state_1.addDelegateTarget)(state, step.owner.name);
@@ -2091,18 +2598,72 @@ class OrchestratorAgent {
2091
2598
  state.finalResponseDraft = lines.join('\n');
2092
2599
  return state.finalResponseDraft;
2093
2600
  }
2094
- return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, {
2601
+ return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, {
2095
2602
  taskIds: createdTasks.map((task) => task.id),
2096
2603
  workflowName: workflowTemplate?.name || null,
2097
2604
  });
2098
2605
  }
2099
- async dispatchQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, context) {
2100
- const maxSteps = 20;
2101
- for (let iteration = 0; iteration < maxSteps; iteration += 1) {
2102
- const task = data.getNextActiveTodoForConversation(conversationId);
2606
+ async dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, context) {
2607
+ // Pending queue seeded from the orchestrator's delegation. These IDs are
2608
+ // the source of truth for the dispatch chain; we run them in the exact
2609
+ // order the orchestrator returned them. If a completion produces a new
2610
+ // TODO (qa_result='fail' auto-fix, insert_task, or re-QA companion),
2611
+ // the new TODO is appended to the front of pending so the inline fix
2612
+ // cycle runs before anything sitting later in the original sequence.
2613
+ // (Codex QA 2026-04-19 #16 / april19fixes.txt item 5.)
2614
+ const pending = context.taskIds.filter((id) => Number.isFinite(id)).map((id) => Number(id));
2615
+ const seen = new Set(pending);
2616
+ // Legacy fallback: if the caller didn't provide taskIds (older code
2617
+ // paths that haven't been migrated yet), seed from the oldest active
2618
+ // TODO. The new local_desktop dispatch path always supplies taskIds.
2619
+ const completedTaskIds = [];
2620
+ const initialTaskCount = pending.length;
2621
+ // Explicit/imported workflows must be allowed to run to completion even
2622
+ // when they legitimately contain dozens of ordered TODOs. Keep the
2623
+ // safety brake, but make it an emergency guard against runaway re-queue
2624
+ // loops rather than a low fixed workflow-length cap.
2625
+ const maxIterations = Math.max(500, (initialTaskCount * 10) + 100);
2626
+ const maxDynamicallyInsertedTasks = Math.max(200, (initialTaskCount * 4) + 50);
2627
+ let dynamicallyInsertedCount = 0;
2628
+ if (pending.length === 0) {
2629
+ const first = data.getNextActiveTodoForConversation(conversationId);
2630
+ if (first) {
2631
+ pending.push(first.id);
2632
+ seen.add(first.id);
2633
+ }
2634
+ }
2635
+ for (let iteration = 0;; iteration += 1) {
2636
+ if (iteration >= maxIterations) {
2637
+ const message = 'The workflow created too many follow-up executions and was stopped to prevent an infinite loop.';
2638
+ state.finalResponseDraft = message;
2639
+ this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxIterations, initialTaskCount, dynamicallyInsertedCount });
2640
+ return message;
2641
+ }
2642
+ if (opts.abortSignal?.aborted) {
2643
+ state.finalResponseDraft = '';
2644
+ return '';
2645
+ }
2646
+ let task;
2647
+ while (pending.length > 0 && !task) {
2648
+ const candidateId = pending.shift();
2649
+ const active = data.getTodoTask(candidateId, 'active');
2650
+ if (active) {
2651
+ task = active;
2652
+ }
2653
+ else if (data.getTodoTask(candidateId, 'completed')) {
2654
+ this.recordOrchestrationAudit(state, 'run_workflow', 'completed', `resume_skipped_completed_task:${candidateId}`, { taskId: candidateId });
2655
+ }
2656
+ }
2103
2657
  if (!task) {
2104
- return await this.finalizeQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, context.workflowName);
2658
+ return await this.finalizeQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, context.workflowName, completedTaskIds);
2105
2659
  }
2660
+ // Snapshot active IDs BEFORE this iteration so the post-iteration
2661
+ // "new TODOs" scan only picks up tasks that were actually created
2662
+ // during this execute() call (fix cycles, insert_task, re-QA
2663
+ // companions). Stale/pre-existing active TODOs from prior turns
2664
+ // are NOT swept in — the dispatcher only executes what the
2665
+ // orchestrator delegated plus inline insertions.
2666
+ const beforeActiveIds = new Set(data.listActiveTodoTasksForConversation(conversationId).map((t) => t.id));
2106
2667
  const owner = task.owner_bot_id
2107
2668
  ? data.getAgentProfile(task.owner_bot_id)
2108
2669
  : this.findAgentByName(task.owner_name || undefined, project?.id || undefined);
@@ -2113,8 +2674,12 @@ class OrchestratorAgent {
2113
2674
  return message;
2114
2675
  }
2115
2676
  const taskPrompt = task.task_prompt?.trim() || task.details || task.title;
2677
+ if (opts.importedPlanRunId) {
2678
+ data.touchImportedPlanRun(opts.importedPlanRunId);
2679
+ }
2116
2680
  const result = await this.runNodeWithRetry('run_workflow', state, async () => this.workflowEngine.execute(taskPrompt, conversationId, owner.id, {
2117
2681
  apiKey: this.resolveApiKey(owner.provider),
2682
+ abortSignal: opts.abortSignal,
2118
2683
  taskId: task.id,
2119
2684
  stepDescription: task.title,
2120
2685
  disableDecomposition: true,
@@ -2122,6 +2687,10 @@ class OrchestratorAgent {
2122
2687
  projectId: project?.id || opts.projectId || state.projectId || null,
2123
2688
  workspacePath: project?.folder?.trim() || undefined,
2124
2689
  persistConversationMessages: false,
2690
+ storedAttachments: opts.storedAttachments,
2691
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
2692
+ importedPlanRunId: opts.importedPlanRunId,
2693
+ cliSessionScopeKey: opts.cliSessionScopeKey,
2125
2694
  onWorkerChunk: opts.onWorkerChunk,
2126
2695
  onProgress: (progress) => {
2127
2696
  if (progress.event === 'step-started') {
@@ -2135,11 +2704,19 @@ class OrchestratorAgent {
2135
2704
  }
2136
2705
  },
2137
2706
  }));
2707
+ if (opts.abortSignal?.aborted || isAbortLikeError(result?.error)) {
2708
+ state.finalResponseDraft = '';
2709
+ return '';
2710
+ }
2138
2711
  const stillActive = data.getTodoTask(task.id, 'active');
2139
2712
  const completed = data.getTodoTask(task.id, 'completed');
2713
+ if (opts.abortSignal?.aborted) {
2714
+ state.finalResponseDraft = '';
2715
+ return '';
2716
+ }
2140
2717
  if (stillActive?.blocker_summary && stillActive.blocker_question) {
2141
2718
  this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', `${owner.name} blocked on TODO: ${task.title}`, { taskId: task.id, blockerSummary: stillActive.blocker_summary });
2142
- return await this.handleBlockedTodoTask(stillActive, opts, state, orchestratorBot, project);
2719
+ return await this.handleBlockedTodoTask(stillActive, opts, state, orchestratorActor, project);
2143
2720
  }
2144
2721
  if (!completed) {
2145
2722
  const error = result.steps.find((step) => step.error)?.error || this.stripWorkerProtocol(result.mergedResult || '') || 'Worker did not complete the queued task.';
@@ -2148,19 +2725,46 @@ class OrchestratorAgent {
2148
2725
  this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { taskId: task.id });
2149
2726
  return message;
2150
2727
  }
2728
+ completedTaskIds.push(completed.id);
2729
+ // Pick up any TODOs that were newly inserted during THIS iteration
2730
+ // (fix cycles, re-QA companions, worker-directed insert_task) —
2731
+ // i.e. active now but not active before the iteration began. They
2732
+ // go to the front of pending so the inline cycle completes before
2733
+ // the original chain continues. Stale/older active TODOs from
2734
+ // prior turns are NOT swept in (they were in beforeActiveIds).
2735
+ const activeAfter = data.listActiveTodoTasksForConversation(conversationId);
2736
+ const newlyInserted = [];
2737
+ for (const t of activeAfter) {
2738
+ if (!beforeActiveIds.has(t.id) && !seen.has(t.id)) {
2739
+ newlyInserted.push(t.id);
2740
+ seen.add(t.id);
2741
+ }
2742
+ }
2743
+ if (newlyInserted.length > 0) {
2744
+ dynamicallyInsertedCount += newlyInserted.length;
2745
+ if (dynamicallyInsertedCount > maxDynamicallyInsertedTasks) {
2746
+ const message = 'The workflow created too many follow-up TODOs and was stopped to prevent an infinite loop.';
2747
+ state.finalResponseDraft = message;
2748
+ this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxDynamicallyInsertedTasks, dynamicallyInsertedCount, initialTaskCount });
2749
+ return message;
2750
+ }
2751
+ pending.unshift(...newlyInserted);
2752
+ }
2151
2753
  }
2152
- const message = 'The workflow exceeded the automatic dispatch limit before completing.';
2153
- state.finalResponseDraft = message;
2154
- this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxSteps: 20 });
2155
- return message;
2156
2754
  }
2157
- async handleBlockedTodoTask(task, opts, state, orchestratorBot, project) {
2755
+ async handleBlockedTodoTask(task, opts, state, orchestratorActor, project) {
2158
2756
  const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
2159
2757
  const artifacts = data.listTodoArtifactsForTask(task.id, 'active').map((item) => item.path_or_ref);
2160
2758
  const completedTasks = data.listCompletedTodoTasksForConversation(task.conversation_id || state.conversationId || '');
2161
2759
  const workerRole = task.task_type || task.next_worker_role || 'general';
2760
+ if (/provider returned a quota\/rate-limit error/i.test(task.blocker_checked || '')) {
2761
+ const exact = task.blocker_summary || 'Provider limit reached.';
2762
+ const response = `Provider returned:\n\n${exact}\n\nFix the provider account limit or wait for the retry window, then resume the plan.`;
2763
+ state.finalResponseDraft = response;
2764
+ return response;
2765
+ }
2162
2766
  const blockedPrompt = (0, orchestrator_blocked_prompt_1.buildBlockedWorkerOrchestratorPrompt)({
2163
- orchestratorName: orchestratorBot.name,
2767
+ orchestratorName: orchestratorActor.name,
2164
2768
  workerName: task.owner_name || 'Worker',
2165
2769
  workerRole,
2166
2770
  taskTitle: task.title,
@@ -2177,7 +2781,14 @@ class OrchestratorAgent {
2177
2781
  });
2178
2782
  let rawResponse = '';
2179
2783
  try {
2180
- rawResponse = await this.runNodeWithRetry('finalize_response', state, async () => this.respondAsClerk(blockedPrompt, 'You are Clerk. Write the user-facing blocker message after work has already started. Be concise, outcome-first, and do not restate the full original request.'));
2784
+ rawResponse = await this.runNodeWithRetry('finalize_response', state, async () => this.runOrchestratorPrompt(blockedPrompt, `You are ${this.orchestratorRuntime.agentName}. Write the user-facing blocker message after work has already started. Be concise, outcome-first, and do not restate the full original request.`, {
2785
+ conversationId: task.conversation_id || state.conversationId || null,
2786
+ orchestratorBot: orchestratorActor.bot || undefined,
2787
+ projectId: project?.id || opts.projectId || state.projectId || null,
2788
+ workspacePath: project?.folder?.trim() || undefined,
2789
+ disableTools: true,
2790
+ abortSignal: opts.abortSignal,
2791
+ }));
2181
2792
  }
2182
2793
  catch (error) {
2183
2794
  this.recordOrchestrationAudit(state, 'finalize_response', 'blocked', `Blocked final response generation failed: ${error?.message || error}`);
@@ -2189,8 +2800,14 @@ class OrchestratorAgent {
2189
2800
  state.finalResponseDraft = response;
2190
2801
  return response;
2191
2802
  }
2192
- async finalizeQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, workflowName) {
2193
- const completedTasks = data.listCompletedTodoTasksForConversation(conversationId).map((task) => ({
2803
+ async finalizeQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, workflowName, completedTaskIds = []) {
2804
+ const ids = completedTaskIds
2805
+ .map((id) => Number(id))
2806
+ .filter((id, index, arr) => Number.isFinite(id) && id > 0 && arr.indexOf(id) === index);
2807
+ const sourceTasks = ids
2808
+ .map((id) => data.getTodoTask(id, 'completed'))
2809
+ .filter((task) => Boolean(task));
2810
+ const completedTasks = sourceTasks.map((task) => ({
2194
2811
  ...task,
2195
2812
  artifactRefs: data.listTodoArtifactsForTask(task.id, 'completed').map((item) => item.path_or_ref),
2196
2813
  }));
@@ -2200,7 +2817,7 @@ class OrchestratorAgent {
2200
2817
  }
2201
2818
  const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
2202
2819
  const finalPrompt = (0, orchestrator_final_response_prompt_1.buildOrchestratorFinalResponsePrompt)({
2203
- orchestratorName: orchestratorBot.name,
2820
+ orchestratorName: orchestratorActor.name,
2204
2821
  projectName: project?.name || null,
2205
2822
  workspacePath: project?.folder || null,
2206
2823
  effectivePolicy: policy,
@@ -2208,7 +2825,14 @@ class OrchestratorAgent {
2208
2825
  });
2209
2826
  let response = '';
2210
2827
  try {
2211
- response = await this.runNodeWithRetry('finalize_response', state, async () => this.respondAsClerk(finalPrompt, 'You are Clerk. Write the final user-facing answer after the workflow is complete. Do not restate the full request. Say what was created, where it is, the concise result, and only any important caveat. Do not claim everything was verified, defect-free, or ready for use unless a completed verification step explicitly established that.'));
2828
+ response = await this.runNodeWithRetry('finalize_response', state, async () => this.runOrchestratorPrompt(finalPrompt, `You are ${this.orchestratorRuntime.agentName}. Write the final user-facing answer after the workflow is complete. Do not restate the full request. Say what was created, where it is, the concise result, and only any important caveat. Do not claim everything was verified, defect-free, or ready for use unless a completed verification step explicitly established that.`, {
2829
+ conversationId,
2830
+ orchestratorBot: orchestratorActor.bot || undefined,
2831
+ projectId: project?.id || opts.projectId || state.projectId || null,
2832
+ workspacePath: project?.folder?.trim() || undefined,
2833
+ disableTools: true,
2834
+ abortSignal: opts.abortSignal,
2835
+ }));
2212
2836
  }
2213
2837
  catch (error) {
2214
2838
  this.recordOrchestrationAudit(state, 'finalize_response', 'blocked', `Final response generation failed: ${error?.message || error}`);
@@ -2223,27 +2847,31 @@ class OrchestratorAgent {
2223
2847
  : 'Completed automatic TODO dispatch.', { completedTaskCount: completedTasks.length });
2224
2848
  return response;
2225
2849
  }
2226
- buildSingleDelegateTodoStep(prompt, owner, orchestratorBot, projectName, workspacePath, effectivePolicy) {
2850
+ buildSingleDelegateTodoStep(prompt, conversationId, owner, orchestratorActor, projectName, workspacePath, effectivePolicy) {
2227
2851
  const taskType = this.normalizeTaskTypeForWorker(owner, 'coding');
2852
+ const recentContext = this.buildRecentConversationContextForWorker(conversationId);
2228
2853
  return {
2229
- title: `${owner.name}: ${this.summarizeTodoTitle(prompt)}`,
2230
- details: `Delegated by ${orchestratorBot.name}. Complete this single-worker request directly.`,
2854
+ title: `${owner.name}: handle the delegated request`,
2855
+ details: `Delegated by ${orchestratorActor.name}. Complete this single-worker request directly.`,
2231
2856
  owner,
2232
2857
  taskType,
2233
2858
  taskPrompt: this.buildWorkerTaskPrompt({
2234
2859
  originalPrompt: prompt,
2235
- stepInstruction: 'Handle this delegated request directly.',
2860
+ stepInstruction: 'Handle the user request from this conversation.',
2236
2861
  owner,
2237
2862
  previousWorker: null,
2238
2863
  nextWorker: null,
2864
+ conversationId,
2239
2865
  projectName,
2240
2866
  workspacePath,
2241
2867
  effectivePolicy,
2868
+ recentContext,
2242
2869
  }),
2243
2870
  successCriteria: `Complete the delegated ${taskType} task and return a concise result summary.`,
2244
2871
  };
2245
2872
  }
2246
- buildTodoStepsFromTemplate(prompt, workflowTemplate, orchestratorBot, projectName, workspacePath, effectivePolicy) {
2873
+ buildTodoStepsFromTemplate(prompt, conversationId, workflowTemplate, orchestratorActor, projectName, workspacePath, effectivePolicy) {
2874
+ const recentContext = this.buildRecentConversationContextForWorker(conversationId);
2247
2875
  const steps = workflowTemplate.steps
2248
2876
  .sort((a, b) => a.order_index - b.order_index)
2249
2877
  .map((step) => {
@@ -2269,14 +2897,315 @@ class OrchestratorAgent {
2269
2897
  owner: step.owner,
2270
2898
  previousWorker: steps[index - 1]?.owner || null,
2271
2899
  nextWorker: steps[index + 1]?.owner || null,
2900
+ conversationId,
2272
2901
  projectName,
2273
2902
  workspacePath,
2274
2903
  effectivePolicy,
2904
+ recentContext,
2275
2905
  }),
2276
2906
  successCriteria: `Complete step ${index + 1} of "${workflowTemplate.name}" and hand off the necessary output.`,
2277
2907
  }));
2278
2908
  }
2279
- buildTodoStepsFromAssignments(prompt, roleAssignments, intent, orchestratorBot, projectId, projectName, workspacePath, effectivePolicy) {
2909
+ buildSelectedWorkflowTemplateHint(workflowTemplate) {
2910
+ return {
2911
+ id: workflowTemplate.id,
2912
+ name: workflowTemplate.name,
2913
+ description: workflowTemplate.description || null,
2914
+ steps: workflowTemplate.steps
2915
+ .slice()
2916
+ .sort((a, b) => a.order_index - b.order_index)
2917
+ .map((step, index) => {
2918
+ const bot = data.getAgentProfile(step.agent_id);
2919
+ return {
2920
+ index,
2921
+ bot_name: bot?.name || step.agent_id,
2922
+ role_priorities: bot ? data.getAgentRolePriorities(bot) : [],
2923
+ saved_instruction: step.instruction?.trim() || null,
2924
+ is_checkpoint: step.is_checkpoint === 1,
2925
+ };
2926
+ }),
2927
+ };
2928
+ }
2929
+ createPendingWorkflowProposal(input) {
2930
+ this.deleteStalePendingWorkflowProposals(input.conversationId, input.orchestratorActor);
2931
+ const workflowArgs = this.buildPendingWorkflowProposalArgs(input.prompt, input.candidateWorkflowPlan, input.selectedWorkflowTemplateHint);
2932
+ if (workflowArgs.steps.length === 0)
2933
+ return [];
2934
+ const existingActive = data.listActiveTodoTasksForConversation(input.conversationId);
2935
+ const result = input.executeCreateWorkflow(workflowArgs, {
2936
+ conversationId: input.conversationId,
2937
+ projectId: input.projectId,
2938
+ orchestratorActor: input.orchestratorActor,
2939
+ resolution: {
2940
+ activeBots: input.activeBots,
2941
+ botsWithActiveTodos: new Set(existingActive.map((task) => task.owner_name).filter(Boolean)),
2942
+ },
2943
+ userPrompt: input.prompt,
2944
+ recentContext: input.recentContext || null,
2945
+ });
2946
+ if (!result || result.kind === 'error' || !Array.isArray(result.tasks))
2947
+ return [];
2948
+ const confidence = this.summarizeCandidateConfidence(input.candidateWorkflowPlan.steps);
2949
+ const stepReasons = input.candidateWorkflowPlan.steps
2950
+ .map((step) => {
2951
+ const role = step.suggested_role ? ` as ${step.suggested_role}` : '';
2952
+ const confidenceLabel = step.confidence || 'low';
2953
+ const reason = step.reason || 'No specific role signal; using configured priority.';
2954
+ return `- ${step.bot_name}${role}: ${confidenceLabel} - ${reason}`;
2955
+ })
2956
+ .join('\n');
2957
+ const details = [
2958
+ 'System-proposed workflow. Awaiting orchestrator approval; workers have not started.',
2959
+ `Source: ${input.candidateWorkflowPlan.source}.`,
2960
+ `Confidence: ${confidence}.`,
2961
+ `Reason: ${input.candidateWorkflowPlan.reason}`,
2962
+ stepReasons ? `Step confidence:\n${stepReasons}` : '',
2963
+ ].join('\n');
2964
+ for (const task of result.tasks) {
2965
+ try {
2966
+ data.editTodoTask(task.id, {
2967
+ details,
2968
+ actor: { actorType: 'orchestrator', actorId: input.orchestratorActor.actorId },
2969
+ });
2970
+ }
2971
+ catch {
2972
+ // Best effort. The task remains visible even if details were not updated.
2973
+ }
2974
+ }
2975
+ return result.tasks.map((task) => task.id);
2976
+ }
2977
+ deleteStalePendingWorkflowProposals(conversationId, orchestratorActor) {
2978
+ const marker = 'System-proposed workflow. Awaiting orchestrator approval; workers have not started.';
2979
+ for (const task of data.listActiveTodoTasksForConversation(conversationId)) {
2980
+ if (!String(task.details || '').includes(marker))
2981
+ continue;
2982
+ try {
2983
+ data.deleteTodoTask(task.id, {
2984
+ actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
2985
+ });
2986
+ }
2987
+ catch {
2988
+ // Leave concurrently touched tasks alone.
2989
+ }
2990
+ }
2991
+ }
2992
+ summarizeCandidateConfidence(steps) {
2993
+ if (steps.length === 0)
2994
+ return 'low';
2995
+ const values = steps.map((step) => step.confidence || 'low');
2996
+ if (values.every((value) => value === 'high'))
2997
+ return 'high';
2998
+ if (values.every((value) => value === 'low'))
2999
+ return 'low';
3000
+ if (values.some((value) => value === 'low'))
3001
+ return 'medium';
3002
+ return 'medium';
3003
+ }
3004
+ buildPendingWorkflowProposalArgs(prompt, candidate, selectedWorkflowTemplateHint) {
3005
+ const target = this.inferWorkflowTargetForTitle(prompt);
3006
+ const templateStepsByIndex = new Map((selectedWorkflowTemplateHint?.steps || []).map((step) => [step.index, step]));
3007
+ return {
3008
+ steps: candidate.steps.map((step, index) => {
3009
+ const role = step.suggested_role || step.role_priorities[0] || '';
3010
+ const action = this.workflowActionForRole(role, prompt);
3011
+ const templateInstruction = templateStepsByIndex.get(step.index)?.saved_instruction || '';
3012
+ const nextStep = candidate.steps[index + 1] || null;
3013
+ const title = this.buildCandidateWorkflowTitle({
3014
+ action,
3015
+ role,
3016
+ target,
3017
+ nextBotName: nextStep?.bot_name || null,
3018
+ });
3019
+ return {
3020
+ bot_name: step.bot_name,
3021
+ role: role || undefined,
3022
+ title,
3023
+ task_instructions: templateInstruction || this.buildCandidateWorkflowInstructions({
3024
+ action,
3025
+ role,
3026
+ target,
3027
+ nextBotName: nextStep?.bot_name || null,
3028
+ nextRole: nextStep?.suggested_role || nextStep?.role_priorities?.[0] || null,
3029
+ }),
3030
+ };
3031
+ }),
3032
+ };
3033
+ }
3034
+ workflowActionForRole(role, prompt) {
3035
+ const normalized = String(role || '').trim().toLowerCase();
3036
+ if (this.roleHasCapability(role, 'prompt_building'))
3037
+ return 'Generate prompt';
3038
+ if (this.roleHasCapability(role, 'qa'))
3039
+ return 'QA';
3040
+ if (this.roleHasCapability(role, 'verify'))
3041
+ return 'Verify';
3042
+ if (this.roleHasCapability(role, 'deployment'))
3043
+ return 'Deploy';
3044
+ if (this.roleHasCapability(role, 'research') || this.roleHasCapability(role, 'design'))
3045
+ return 'Plan';
3046
+ if (this.roleHasCapability(role, 'coding')) {
3047
+ const promptText = String(prompt || '').toLowerCase();
3048
+ if (/\b(clean\s+up|improve|redesign|ui\/?ux|make .*better|polish)\b/.test(promptText))
3049
+ return 'Improve';
3050
+ if (/\b(fix|repair|correct)\b/.test(promptText))
3051
+ return 'Fix';
3052
+ if (/\b(update|modify|edit|change)\b/.test(promptText))
3053
+ return 'Update';
3054
+ return 'Build';
3055
+ }
3056
+ if (/\b(qa|quality assurance|review|test)\b/.test(normalized))
3057
+ return 'QA';
3058
+ if (/\b(verify|verification|validate)\b/.test(normalized))
3059
+ return 'Verify';
3060
+ if (/\b(research|brainstorm|plan|planning|design)\b/.test(normalized))
3061
+ return 'Plan';
3062
+ if (/\b(deploy|deployment|release|ship)\b/.test(normalized))
3063
+ return 'Deploy';
3064
+ if (/\b(code|coding|build|implement|create|write|develop)\b/.test(normalized)) {
3065
+ const promptText = String(prompt || '').toLowerCase();
3066
+ if (/\b(clean\s+up|improve|redesign|ui\/?ux|make .*better|polish)\b/.test(promptText))
3067
+ return 'Improve';
3068
+ if (/\b(fix|repair|correct)\b/.test(promptText))
3069
+ return 'Fix';
3070
+ if (/\b(update|modify|edit|change)\b/.test(promptText))
3071
+ return 'Update';
3072
+ return 'Build';
3073
+ }
3074
+ return 'Handle';
3075
+ }
3076
+ buildCandidateWorkflowTitle(input) {
3077
+ if (this.roleHasCapability(input.role, 'prompt_building')) {
3078
+ return input.nextBotName
3079
+ ? `Generate ${input.nextBotName} handoff for ${input.target}`
3080
+ : `Generate prompt for ${input.target}`;
3081
+ }
3082
+ return `${input.action} ${input.target}`;
3083
+ }
3084
+ buildCandidateWorkflowInstructions(input) {
3085
+ if (this.roleHasCapability(input.role, 'prompt_building')) {
3086
+ const next = input.nextBotName
3087
+ ? `${input.nextBotName}${input.nextRole ? ` (${input.nextRole})` : ''}`
3088
+ : 'the next worker';
3089
+ return [
3090
+ `Create a professional, detailed worker prompt for ${next} to handle ${input.target}.`,
3091
+ `Include the target artifact/path, the current issue or requested change, exact expected behavior, scope limits, constraints, and acceptance checks.`,
3092
+ 'Put the finished prompt in complete_worker_task.handoff_prompt so the next worker can act without guessing.',
3093
+ ].join(' ');
3094
+ }
3095
+ const nextLine = input.nextBotName
3096
+ ? ` When done, hand off enough detail for ${input.nextBotName}${input.nextRole ? ` (${input.nextRole})` : ''} to continue without guessing.`
3097
+ : '';
3098
+ return `${input.action} ${input.target} for the current user request.${nextLine}`;
3099
+ }
3100
+ roleHasCapability(role, capabilityId) {
3101
+ return (0, capabilities_1.matchCapabilityIds)([String(role || '')]).includes(capabilityId);
3102
+ }
3103
+ inferWorkflowTargetForTitle(prompt) {
3104
+ const text = String(prompt || '');
3105
+ const fileMatch = text.match(/\b([A-Za-z0-9_-]+\.(?:html|tsx?|jsx?|css|md|txt|json))\b/);
3106
+ if (fileMatch?.[1])
3107
+ return fileMatch[1];
3108
+ if (/\boutfitter\b/i.test(text) || /\belk hunting\b/i.test(text))
3109
+ return 'outfitter page';
3110
+ if (/\b(web\s?page|website|html|landing page|page)\b/i.test(text))
3111
+ return 'requested page';
3112
+ if (/\binstall(?:er)?\b/i.test(text))
3113
+ return 'installer';
3114
+ if (/\brelease|deploy|download\b/i.test(text))
3115
+ return 'release';
3116
+ return 'request';
3117
+ }
3118
+ buildExplicitBotSequenceCandidatePlan(prompt, activeBots) {
3119
+ const text = String(prompt || '');
3120
+ if (!text.trim())
3121
+ return null;
3122
+ const matches = [];
3123
+ for (const bot of activeBots) {
3124
+ const escaped = bot.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3125
+ const match = new RegExp(`\\b${escaped}\\b`, 'i').exec(text);
3126
+ if (match && Number.isFinite(match.index)) {
3127
+ matches.push({ bot, index: match.index });
3128
+ }
3129
+ }
3130
+ const ordered = matches
3131
+ .sort((a, b) => a.index - b.index)
3132
+ .filter((match, index, arr) => arr.findIndex((other) => other.bot.id === match.bot.id) === index);
3133
+ if (ordered.length < 2)
3134
+ return null;
3135
+ if (!this.hasExplicitBotWorkflowSignal(text))
3136
+ return null;
3137
+ return {
3138
+ source: 'explicit_bot_sequence',
3139
+ reason: `User explicitly mentioned multiple bots in order: ${ordered.map((item) => item.bot.name).join(' -> ')}.`,
3140
+ steps: ordered.map((item, index) => {
3141
+ const inference = this.inferSuggestedRoleNearBot(text, item.index, ordered[index + 1]?.index, item.bot);
3142
+ return {
3143
+ index,
3144
+ bot_name: item.bot.name,
3145
+ role_priorities: data.getAgentRolePriorities(item.bot),
3146
+ suggested_role: inference.role,
3147
+ confidence: inference.confidence,
3148
+ reason: inference.reason,
3149
+ };
3150
+ }),
3151
+ };
3152
+ }
3153
+ hasExplicitBotWorkflowSignal(prompt) {
3154
+ const text = prompt.toLowerCase();
3155
+ if (/\b(have|use|ask|tell|delegate|assign|workflow)\b/.test(text))
3156
+ return true;
3157
+ if (/\bthen\b|\bafter that\b|\bfollowed by\b/.test(text))
3158
+ return true;
3159
+ return /\b(code|coding|build|implement|create|write|develop|edit|update|modify|fix|prompt|qa|quality assurance|review|test|testing|verify|verification|validate|research|plan|brainstorm)\b/.test(text);
3160
+ }
3161
+ inferSuggestedRoleNearBot(prompt, startIndex, nextStartIndex, bot) {
3162
+ const roles = data.getAgentRolePriorities(bot);
3163
+ const segmentEnd = nextStartIndex === undefined ? prompt.length : nextStartIndex;
3164
+ const segment = prompt.slice(startIndex, segmentEnd).toLowerCase();
3165
+ const roleKeywordGroups = [
3166
+ {
3167
+ keywords: /\b(?:create|write|generate|draft|prepare|build|make)\s+(?:a\s+|the\s+)?(?:better\s+|professional\s+|detailed\s+)?(?:user\s+|worker\s+|handoff\s+)?prompt\b|\bhandoff\s+prompt\b|\bprompt\s+for\b/i,
3168
+ capabilityId: 'prompt_building',
3169
+ reason: 'User asked this worker to create or prepare a prompt/handoff.',
3170
+ },
3171
+ { keywords: /\b(qa|quality assurance|review|test|testing|audit)\b/i, capabilityId: 'qa', reason: 'User asked this worker to review, test, or QA.' },
3172
+ { keywords: /\b(verify|verification|validate|validation)\b/i, capabilityId: 'verify', reason: 'User asked this worker to verify or validate.' },
3173
+ { keywords: /\b(deploy|deployment|release|ship|publish)\b/i, capabilityId: 'deployment', reason: 'User asked this worker to deploy, release, or publish.' },
3174
+ { keywords: /\b(code|coding|build|implement|create|write|develop|edit|update|modify|fix|clean\s+up|improve|redesign)\b/i, capabilityId: 'coding', reason: 'User asked this worker to create, modify, or fix an artifact.' },
3175
+ { keywords: /\b(design|ui\/?ux|ux|visual|layout)\b/i, capabilityId: 'design', reason: 'User asked this worker to handle design or UI/UX planning.' },
3176
+ { keywords: /\b(research|investigate|analy[sz]e|analysis|brainstorm|plan|planning)\b/i, capabilityId: 'research', reason: 'User asked this worker to research or plan.' },
3177
+ ];
3178
+ for (const group of roleKeywordGroups) {
3179
+ if (!group.keywords.test(segment))
3180
+ continue;
3181
+ const matchedRole = (0, capabilities_1.findRoleLabelForCapability)(roles, group.capabilityId);
3182
+ const botHasCapability = roles.some((role) => (0, capabilities_1.matchCapabilityIds)([role]).includes(group.capabilityId));
3183
+ return {
3184
+ role: matchedRole,
3185
+ confidence: botHasCapability ? 'high' : 'medium',
3186
+ reason: botHasCapability
3187
+ ? group.reason
3188
+ : `${group.reason} The worker was named explicitly; role is informational because it is not in that worker's configured priorities.`,
3189
+ };
3190
+ }
3191
+ return {
3192
+ role: roles[0] || null,
3193
+ confidence: 'low',
3194
+ reason: 'No specific capability keyword was found near this bot name; using the worker priority list.',
3195
+ };
3196
+ }
3197
+ reportCandidateWorkflowProgress(opts, candidate) {
3198
+ const names = candidate.steps
3199
+ .map((step) => step.suggested_role ? `${step.bot_name} (${step.suggested_role})` : step.bot_name)
3200
+ .filter(Boolean);
3201
+ const prefix = candidate.source === 'explicit_bot_sequence'
3202
+ ? 'Detected requested bot sequence'
3203
+ : 'Detected requested workflow';
3204
+ const summary = names.length > 0 ? `${prefix}: ${names.join(' -> ')}.` : prefix + '.';
3205
+ opts.onProgress(`Orchestrator:: ${summary} Awaiting orchestrator approval before starting workers.`);
3206
+ }
3207
+ buildTodoStepsFromAssignments(prompt, conversationId, roleAssignments, intent, orchestratorActor, projectId, projectName, workspacePath, effectivePolicy) {
3208
+ const recentContext = this.buildRecentConversationContextForWorker(conversationId);
2280
3209
  const roleOrder = this.inferWorkflowRoleOrder(prompt, roleAssignments, intent);
2281
3210
  const owners = roleOrder
2282
3211
  .map((role) => {
@@ -2289,7 +3218,7 @@ class OrchestratorAgent {
2289
3218
  .filter((item) => Boolean(item));
2290
3219
  return owners.map((item, index) => ({
2291
3220
  title: `${item.agent.name}: ${this.defaultTodoTitleForRole(item.role, prompt)}`,
2292
- details: `Workflow step assigned by ${orchestratorBot.name}.`,
3221
+ details: `Workflow step assigned by ${orchestratorActor.name}.`,
2293
3222
  owner: item.agent,
2294
3223
  taskType: this.normalizeTaskTypeForWorker(item.agent, item.role === 'research' ? 'research' : item.role === 'qa' ? 'qa' : 'coding'),
2295
3224
  taskPrompt: this.buildWorkerTaskPrompt({
@@ -2298,80 +3227,75 @@ class OrchestratorAgent {
2298
3227
  owner: item.agent,
2299
3228
  previousWorker: owners[index - 1]?.agent || null,
2300
3229
  nextWorker: owners[index + 1]?.agent || null,
3230
+ conversationId,
2301
3231
  projectName: projectName || null,
2302
3232
  workspacePath: workspacePath || null,
2303
3233
  effectivePolicy: effectivePolicy || data.getEffectiveOrchestrationPolicy(projectId),
3234
+ recentContext,
2304
3235
  }),
2305
3236
  successCriteria: this.defaultSuccessCriteriaForRole(item.role),
2306
3237
  }));
2307
3238
  }
2308
- inferWorkflowRoleOrder(prompt, assignments, intent) {
2309
- const explicitResearch = /\b(research|brainstorm|plan|planning|analyze|investigate|design|architect)\b/i.test(prompt);
2310
- const explicitCoding = /\b(code|codes|coded|coding|build|builds|building|implement|implements|implementing|create|creates|creating|fix|fixes|fixing|update|updates|updating|edit|editing|change|changes|changing)\b/i.test(prompt);
2311
- const explicitQa = /\b(qa|review|test|verify|verification|audit)\b/i.test(prompt);
2312
- const requestedRoles = [];
2313
- if (explicitResearch && assignments.research)
2314
- requestedRoles.push('research');
2315
- if (explicitCoding && assignments.coding)
2316
- requestedRoles.push('coding');
2317
- if (explicitQa && assignments.qa)
2318
- requestedRoles.push('qa');
2319
- if (requestedRoles.length > 0)
2320
- return requestedRoles;
2321
- if (intent.intent === 'review' && assignments.qa)
2322
- return ['qa'];
2323
- if (intent.intent === 'brainstorm' && assignments.research)
2324
- return ['research'];
2325
- const fallback = [];
2326
- if (assignments.research && /\b(research|brainstorm|plan|planning)\b/i.test(prompt))
2327
- fallback.push('research');
2328
- if (assignments.coding)
2329
- fallback.push('coding');
2330
- if (assignments.qa)
2331
- fallback.push('qa');
2332
- if (fallback.length > 0)
2333
- return Array.from(new Set(fallback));
3239
+ /**
3240
+ * @deprecated Phase B of orchestration-plan.txt.
3241
+ * Kept only to avoid breaking the non-local_desktop (connected) code paths
3242
+ * that still call it. In local_desktop mode the LLM-tool-dispatch path
3243
+ * makes this regex unreachable. Remove after connected-mode orchestration
3244
+ * is migrated to tool dispatch.
3245
+ */
3246
+ /**
3247
+ * DEPRECATED — returns []. Previously inferred workflow step order from
3248
+ * prompt prose via regex. Codex audit HIGH #6: runtime must not scan
3249
+ * English to decide workflow shape. The tool-dispatch path's
3250
+ * create_workflow is the structured replacement — the orchestrator LLM
3251
+ * decides the ordered role list from the prompt and bot priorities and
3252
+ * emits structured steps. Kept as a stub so any remaining legacy caller
3253
+ * produces an empty role order (which downstream callers handle by
3254
+ * falling through to their no-role-inferred branch).
3255
+ */
3256
+ inferWorkflowRoleOrder(_prompt, _assignments, _intent) {
2334
3257
  return [];
2335
3258
  }
2336
3259
  normalizeTaskTypeForWorker(owner, fallback) {
2337
- const roleClass = String(owner.role_class || owner.role_label || '').trim().toLowerCase();
2338
- if (roleClass === 'qa' || roleClass === 'review')
3260
+ const inferred = (0, capabilities_1.inferTaskTypeFromCapabilities)(null, data.getAgentRolePriorities(owner), fallback || 'coding');
3261
+ if (['qa', 'research', 'verify', 'coding', 'deploy'].includes(inferred)) {
3262
+ return inferred;
3263
+ }
3264
+ if (this.agentHasAnyRole(owner, ['qa', 'review', 'reviewer']))
2339
3265
  return 'qa';
2340
- if (roleClass === 'research' || roleClass === 'brainstorm' || roleClass === 'planning' || roleClass === 'plan')
3266
+ if (this.agentHasAnyRole(owner, ['research', 'researcher', 'analyst', 'advisor', 'brainstorm', 'brainstorming', 'planning', 'plan', 'design']))
2341
3267
  return 'research';
2342
- if (roleClass === 'verify' || roleClass === 'verification')
3268
+ if (this.agentHasAnyRole(owner, ['verify', 'verification']))
2343
3269
  return 'verify';
2344
- if (roleClass === 'deploy' || roleClass === 'deployment')
2345
- return 'deploy';
2346
- if (roleClass === 'coding' || roleClass === 'code' || roleClass === 'builder' || roleClass === 'developer' || roleClass === 'engineer')
3270
+ if (this.agentHasAnyRole(owner, ['coding', 'code', 'builder', 'developer', 'engineer']))
2347
3271
  return 'coding';
3272
+ if (this.agentHasAnyRole(owner, ['deploy', 'deployment']))
3273
+ return 'deploy';
2348
3274
  return (fallback || 'coding');
2349
3275
  }
2350
3276
  buildWorkerTaskPrompt(input) {
2351
- const workerRole = String(input.owner.role_label || input.owner.role_class || 'general').trim() || 'general';
3277
+ const workerRole = this.getOrchestrationRoleLabel(input.owner);
2352
3278
  const taskType = this.normalizeTaskTypeForWorker(input.owner, null);
3279
+ const bootstrapHistoryFilePath = this.prepareBootstrapHistoryFile(input.conversationId);
3280
+ const isFirstInChain = !input.previousWorker;
3281
+ const includeOriginalPrompt = this.shouldIncludeOriginalPromptForWorker(input.owner, isFirstInChain);
2353
3282
  const composedTask = [
2354
3283
  `Step instruction: ${input.stepInstruction}`,
2355
3284
  taskType === 'qa'
2356
- ? 'QA scope: Use PREVIOUS WORKER HANDOFF, queued task details, and explicit success criteria as the source of truth for this QA pass. Only search for broader context if those inputs are incomplete.'
3285
+ ? 'QA scope: Verify the delivered work against the assigned task, previous handoff, explicit success criteria, and original user request when provided. Use the tools and checks needed for that assignment.'
2357
3286
  : taskType === 'verify'
2358
3287
  ? 'Verification scope: Compare the delivered work against the original user request and the explicit success criteria.'
2359
3288
  : '',
2360
- String(input.nextWorker?.role_label || input.nextWorker?.role_class || '').trim().toLowerCase() === 'qa'
2361
- ? 'QA handoff rule: Scope the handoff to static artifact/code review, concrete implementation claims, and explicit pass/fail checks. Do not ask the next QA worker to open a browser, capture screenshots, test desktop/mobile breakpoints, check animations or hover effects, or re-compare against the full original request unless the user explicitly asked for runtime verification.'
2362
- : '',
2363
- String(input.nextWorker?.role_label || input.nextWorker?.role_class || '').trim().toLowerCase() === 'qa'
2364
- ? 'QA handoff rule: Do not ask the next QA worker to use memory search, image analysis, or broad codebase discovery unless a specific missing expectation cannot be checked from the artifact and nearby implementation.'
3289
+ this.agentHasAnyRole(input.nextWorker, ['qa', 'review', 'reviewer'])
3290
+ ? 'QA handoff rule: Include concrete implementation claims, artifact paths, known caveats, and recommended checks. Do not narrow the next QA worker beyond the user request, workflow instruction, or explicit success criteria.'
2365
3291
  : '',
2366
- taskType === 'qa'
2367
- ? null
3292
+ this.agentHasAnyRole(input.nextWorker, ['qa', 'review', 'reviewer'])
3293
+ ? 'QA handoff rule: Let QA decide which available tools or runtime checks are needed for the assigned verification.'
2368
3294
  : '',
2369
- taskType === 'qa'
2370
- ? null
2371
- : 'Original user request (reference only):',
2372
- taskType === 'qa'
2373
- ? null
2374
- : input.originalPrompt,
3295
+ isFirstInChain && input.recentContext?.trim() ? 'Recent conversation context:' : null,
3296
+ isFirstInChain && input.recentContext?.trim() ? input.recentContext.trim() : null,
3297
+ includeOriginalPrompt ? 'Original user request:' : null,
3298
+ includeOriginalPrompt ? input.originalPrompt : null,
2375
3299
  ].join('\n');
2376
3300
  return (0, worker_operating_prompt_1.buildWorkerOperatingPrompt)({
2377
3301
  workerName: input.owner.name,
@@ -2386,26 +3310,60 @@ class OrchestratorAgent {
2386
3310
  defaultLine: 'No confirmed special policy is set.',
2387
3311
  }),
2388
3312
  previousWorkerName: input.previousWorker?.name || null,
2389
- previousWorkerRole: input.previousWorker ? String(input.previousWorker.role_label || input.previousWorker.role_class || 'general') : null,
3313
+ previousWorkerRole: input.previousWorker ? this.getOrchestrationRoleLabel(input.previousWorker) : null,
2390
3314
  nextWorkerName: input.nextWorker?.name || null,
2391
- nextWorkerRole: input.nextWorker ? String(input.nextWorker.role_label || input.nextWorker.role_class || 'general') : null,
3315
+ nextWorkerRole: input.nextWorker ? this.getOrchestrationRoleLabel(input.nextWorker) : null,
3316
+ bootstrapHistoryFilePath,
3317
+ capabilityInstructions: (0, capabilities_1.buildCapabilityInstructionBlock)(data.getAgentRolePriorities(input.owner)),
3318
+ });
3319
+ }
3320
+ buildRecentConversationContextForWorker(conversationId) {
3321
+ const contextWindow = (0, context_window_1.getPromptContextWindow)(conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
3322
+ const lines = [];
3323
+ const summary = contextWindow.summary?.summary_text?.trim();
3324
+ if (summary) {
3325
+ lines.push('[Rolling Summary]');
3326
+ lines.push(contextWindow.carriedForward ? '(carried forward from the previous conversation in this topic)' : '');
3327
+ lines.push(summary);
3328
+ }
3329
+ if (contextWindow.turns.length > 0) {
3330
+ lines.push('[Recent Turns]');
3331
+ lines.push((0, context_window_1.formatTurnsForPrompt)(contextWindow.turns));
3332
+ }
3333
+ if (lines.length === 0) {
3334
+ const currentTurns = (0, context_window_1.getRecentTurns)(conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
3335
+ if (currentTurns.length > 0) {
3336
+ lines.push('[Recent Turns]');
3337
+ lines.push((0, context_window_1.formatTurnsForPrompt)(currentTurns));
3338
+ }
3339
+ }
3340
+ return lines.filter((line) => line !== '').join('\n').slice(0, 10000);
3341
+ }
3342
+ prepareBootstrapHistoryFile(conversationId) {
3343
+ const normalizedConversationId = String(conversationId || '').trim();
3344
+ if (!normalizedConversationId)
3345
+ return null;
3346
+ const topicId = data.getPrimaryTopicIdForConversation(normalizedConversationId) || undefined;
3347
+ return (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
3348
+ conversationId: normalizedConversationId,
3349
+ topicId,
2392
3350
  });
2393
3351
  }
2394
- defaultTodoTitleForRole(role, prompt) {
3352
+ defaultTodoTitleForRole(role, _prompt) {
2395
3353
  if (role === 'research')
2396
- return `Research and plan: ${this.summarizeTodoTitle(prompt)}`;
3354
+ return 'Research and plan the user request';
2397
3355
  if (role === 'qa')
2398
- return `QA and review: ${this.summarizeTodoTitle(prompt)}`;
2399
- return `Build and implement: ${this.summarizeTodoTitle(prompt)}`;
3356
+ return 'QA and review the delivered work';
3357
+ return 'Build and implement the requested work';
2400
3358
  }
2401
- defaultStepInstructionForRole(role, prompt) {
3359
+ defaultStepInstructionForRole(role, _prompt) {
2402
3360
  if (role === 'research') {
2403
- return 'Research, brainstorm, or plan the work so the implementation handoff is specific and actionable.';
3361
+ return 'Research, brainstorm, or plan the user request. Hand off whatever context the next worker needs to continue.';
2404
3362
  }
2405
3363
  if (role === 'qa') {
2406
- return 'QA the completed work against the previous worker handoff, delivered artifact, and explicit success criteria. Focus on artifact/code review only. Do not do browser checks, image analysis, memory search, or broad codebase discovery unless runtime verification was explicitly requested or the handoff is missing a specific fact you cannot otherwise inspect. If issues remain, prepare a precise fix handoff for the previous worker.';
3364
+ return 'QA the completed work against the assigned task, previous worker handoff, delivered artifact, explicit success criteria, and original user request when provided. Use the available tools and checks needed for the assignment. If issues remain, prepare a precise fix handoff for the previous worker.';
2407
3365
  }
2408
- return `Implement the requested work directly. Keep the original request in scope: ${prompt}`;
3366
+ return 'Implement the assigned work using the current task and previous worker handoff as context.';
2409
3367
  }
2410
3368
  defaultSuccessCriteriaForRole(role) {
2411
3369
  if (role === 'research')
@@ -2439,10 +3397,65 @@ class OrchestratorAgent {
2439
3397
  return scopedMatch;
2440
3398
  return allAgents.find((agent) => agent.name.trim().toLowerCase() === normalized);
2441
3399
  }
3400
+ resolveDefaultFallbackAgent(projectId, orchestratorBot, roleAssignments) {
3401
+ const candidates = [];
3402
+ const defaultAgent = data.getDefaultAgentProfile();
3403
+ if (defaultAgent)
3404
+ candidates.push(defaultAgent);
3405
+ if (roleAssignments.coding)
3406
+ candidates.push(this.findAgentByName(roleAssignments.coding, projectId));
3407
+ const project = projectId ? data.getProject(projectId) : undefined;
3408
+ const projectScoped = project?.bot_ids?.length
3409
+ ? data.listAgentProfiles().filter((agent) => project.bot_ids.includes(agent.id))
3410
+ : data.listAgentProfiles();
3411
+ candidates.push(...projectScoped.filter((agent) => !(0, orchestrator_profile_1.isOrchestratorProfile)(agent)));
3412
+ return candidates.find((agent) => !!agent
3413
+ && agent.id !== orchestratorBot.id
3414
+ && !(0, orchestrator_profile_1.isOrchestratorProfile)(agent));
3415
+ }
2442
3416
  async executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state, options) {
2443
- this.recordOrchestrationAudit(state, 'execute_direct', 'executed', `Selected orchestrator bot ${orchestratorBot.name} kept the work.`, { orchestratorBotId: orchestratorBot.id, orchestratorBotName: orchestratorBot.name });
2444
- this.reportHiddenRoleProgress(opts, 'orchestrator', `${orchestratorBot.name} is working on the request`);
3417
+ const executionLabel = this.orchestratorRuntime.kind === 'clerk'
3418
+ ? 'Clerk kept the work.'
3419
+ : `Selected orchestrator bot ${orchestratorBot.name} kept the work.`;
3420
+ this.recordOrchestrationAudit(state, 'execute_direct', 'executed', executionLabel, {
3421
+ orchestratorBotId: orchestratorBot.id,
3422
+ orchestratorBotName: orchestratorBot.name,
3423
+ runtimeKind: this.orchestratorRuntime.kind,
3424
+ });
3425
+ this.reportHiddenRoleProgress(opts, 'orchestrator', this.orchestratorRuntime.kind === 'clerk'
3426
+ ? 'Orchestrator is working on the request'
3427
+ : `${orchestratorBot.name} is working on the request`);
3428
+ if (this.orchestratorRuntime.kind === 'clerk') {
3429
+ try {
3430
+ const response = await this.executeClerkOwnedWorkDirect(prompt, conversationId, orchestratorBot, project, opts);
3431
+ if (!response.trim()) {
3432
+ return {
3433
+ ok: false,
3434
+ response: 'Orchestrator returned an empty response.',
3435
+ error: 'Worker returned an empty response.',
3436
+ };
3437
+ }
3438
+ return this.finalizeOwnedExecutionResult({
3439
+ id: `orchestrator-direct-${Date.now()}`,
3440
+ conversationId,
3441
+ status: 'completed',
3442
+ steps: [],
3443
+ mergedResult: response,
3444
+ startedAt: Date.now(),
3445
+ completedAt: Date.now(),
3446
+ }, prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state, options);
3447
+ }
3448
+ catch (err) {
3449
+ const failureReason = err?.message || 'Unknown error';
3450
+ return {
3451
+ ok: false,
3452
+ response: `Orchestrator encountered an issue: ${failureReason}`,
3453
+ error: failureReason,
3454
+ };
3455
+ }
3456
+ }
2445
3457
  const result = await this.runNodeWithRetry('execute_direct', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
3458
+ abortSignal: opts.abortSignal,
2446
3459
  disableDecomposition: true,
2447
3460
  isOrchestrated: false,
2448
3461
  projectId: project?.id || state.projectId || null,
@@ -2452,6 +3465,7 @@ class OrchestratorAgent {
2452
3465
  // also persist would create duplicate assistant messages.
2453
3466
  persistConversationMessages: false,
2454
3467
  workerMode: false,
3468
+ storedAttachments: opts.storedAttachments,
2455
3469
  onWorkerChunk: opts.onWorkerChunk,
2456
3470
  onProgress: (progress) => {
2457
3471
  if (progress.event === 'step-failed') {
@@ -2521,12 +3535,15 @@ class OrchestratorAgent {
2521
3535
  (0, state_1.addHelperRoleUsage)(state, 'dispatch_controller');
2522
3536
  (0, state_1.addDelegateTarget)(state, delegateBot.name);
2523
3537
  const result = await this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(delegationPrompt, conversationId, delegateBot.id, {
3538
+ abortSignal: opts.abortSignal,
2524
3539
  disableDecomposition: true,
2525
3540
  isOrchestrated: false,
2526
3541
  workerMode: false,
2527
3542
  projectId: project?.id || state.projectId || null,
2528
3543
  workspacePath: project?.folder?.trim() || undefined,
2529
3544
  persistConversationMessages: true,
3545
+ storedAttachments: opts.storedAttachments,
3546
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
2530
3547
  onWorkerChunk: opts.onWorkerChunk,
2531
3548
  onProgress: (progress) => {
2532
3549
  if (progress.event === 'step-started') {
@@ -2551,47 +3568,103 @@ class OrchestratorAgent {
2551
3568
  return 'research';
2552
3569
  return 'discuss';
2553
3570
  }
2554
- inferRequestedWorkflowRoles(prompt, roleAssignments) {
2555
- const normalized = String(prompt || '').toLowerCase();
2556
- const explicitResearch = /\b(brain|gpt|research(?:es|ed|ing)?|investigat(?:e|es|ed|ing)|analy[sz](?:e|es|ed|ing)|analysis|sound idea|review the idea|pressure[- ]?test|brainstorm(?:ed|ing)?)\b/.test(normalized)
2557
- || /\b(best idea|best approach|not sure what to do)\b/.test(normalized);
2558
- const explicitQa = /\b(john|codex|qa(?:['’/-]?d|['’/-]?ed|['’/-]?ing)?\b|quality assurance|test(?:ing)?|verify|review after|review it after|qa it|then qa|pass\/fail)\b/.test(normalized);
2559
- const explicitCoding = /\b(ben|claude|code|build|implement|create|write|fix|edit|html|page|ui|ux|frontend|backend|file)\b/.test(normalized);
2560
- const onlyCodingAndQa = /\bonly\s+coding\s+and\s+qa\b/.test(normalized) || /\bno\s+research\b/.test(normalized);
2561
- const mentionsWorkflow = /\b(workflow|team|todo|handoff|hand off|first\b.*\bthen\b|send .* then|after qa|after john|use my team)\b/.test(normalized);
2562
- const explicitCoderQaPair = explicitCoding && explicitQa;
2563
- const explicitResearchChain = explicitResearch && (explicitCoding || explicitQa);
2564
- if (!mentionsWorkflow && !explicitCoderQaPair && !explicitResearchChain) {
2565
- return [];
3571
+ /**
3572
+ * Builds the chunk-forwarding callback used by both the bot path and the
3573
+ * non-bot path of runOrchestratorPrompt. Wraps the exported helper.
3574
+ */
3575
+ buildNarrationStreamGate(onNarrationChunk) {
3576
+ return buildNarrationStreamGate(onNarrationChunk);
3577
+ }
3578
+ async runOrchestratorPrompt(userPrompt, systemPrompt, opts) {
3579
+ if (this.orchestratorRuntime.kind === 'bot' && opts.orchestratorBot) {
3580
+ // Bot-backed orchestrator: stream chunks via the workflow engine.
3581
+ // The workflow engine receives onWorkerChunk events of type 'chunk'
3582
+ // for normal streaming text from the bot. We wrap onNarrationChunk
3583
+ // in the same gate that the non-bot path uses, so the sentinel/JSON
3584
+ // protection logic is consistent.
3585
+ const narrationGate = opts.onNarrationChunk
3586
+ ? this.buildNarrationStreamGate(opts.onNarrationChunk)
3587
+ : null;
3588
+ const result = await this.workflowEngine.execute(userPrompt, opts.conversationId, opts.orchestratorBot.id, {
3589
+ abortSignal: opts.abortSignal,
3590
+ disableDecomposition: true,
3591
+ disableTools: opts.disableTools === true,
3592
+ isOrchestrated: false,
3593
+ projectId: opts.projectId ?? null,
3594
+ workspacePath: opts.workspacePath,
3595
+ persistConversationMessages: false,
3596
+ workerMode: false,
3597
+ storedAttachments: opts.storedAttachments,
3598
+ systemPromptOverride: systemPrompt,
3599
+ ...(narrationGate ? {
3600
+ onWorkerChunk: async (event) => {
3601
+ // Only forward 'chunk' events (the bot's streaming text) into
3602
+ // the narration gate. Other events (step_start, tool_call, etc.)
3603
+ // are not narration content.
3604
+ if (event.type === 'chunk' && event.text) {
3605
+ await narrationGate(event.text);
3606
+ }
3607
+ },
3608
+ } : {}),
3609
+ });
3610
+ this.setLastResponseMeta({
3611
+ agentName: opts.orchestratorBot.name,
3612
+ botId: opts.orchestratorBot.id,
3613
+ modelLabel: this.orchestratorRuntime.modelLabel,
3614
+ });
3615
+ return this.stripWorkerProtocol(result.mergedResult || '');
2566
3616
  }
2567
- const roles = [];
2568
- if (explicitResearch && !onlyCodingAndQa)
2569
- roles.push('research');
2570
- if (explicitCoding || explicitCoderQaPair || explicitResearchChain)
2571
- roles.push('coding');
2572
- if (explicitQa || explicitCoderQaPair || explicitResearchChain)
2573
- roles.push('qa');
2574
- if (roles.length >= 2)
2575
- return roles;
2576
- if (mentionsWorkflow && roleAssignments.coding && roleAssignments.qa) {
2577
- return roleAssignments.research && !onlyCodingAndQa
2578
- ? ['research', 'coding', 'qa']
2579
- : ['coding', 'qa'];
2580
- }
2581
- return roles;
3617
+ // Pattern C: stream the front-door LLM response through the narration gate.
3618
+ const onChunk = opts.onNarrationChunk
3619
+ ? this.buildNarrationStreamGate(opts.onNarrationChunk)
3620
+ : undefined;
3621
+ const response = await this.orchestratorRuntime.llm.chat({
3622
+ messages: [{ role: 'user', content: userPrompt }],
3623
+ system: systemPrompt,
3624
+ stream: !!onChunk,
3625
+ runtimeMode: 'local_desktop',
3626
+ abortSignal: opts.abortSignal,
3627
+ ...(onChunk ? { onChunk } : {}),
3628
+ });
3629
+ this.setLastResponseMeta({
3630
+ agentName: this.isLocalDesktopRuntime() ? 'Orchestrator' : this.orchestratorRuntime.agentName,
3631
+ botId: this.orchestratorRuntime.botId,
3632
+ modelLabel: this.orchestratorRuntime.modelLabel,
3633
+ });
3634
+ return String(response.content || '').trim();
3635
+ }
3636
+ /**
3637
+ * @deprecated Phase B of orchestration-plan.txt.
3638
+ * Unreachable from local_desktop — that path always uses the LLM-tool
3639
+ * dispatch in orchestration/dispatch-runner. Retained only for the
3640
+ * non-local_desktop (connected/server) code paths that have not yet
3641
+ * been migrated. Remove after connected-mode migration.
3642
+ */
3643
+ /**
3644
+ * DEPRECATED — returns []. Previously scanned the user prompt via regex
3645
+ * for role keywords (research/code/fix/qa/review/verify/after qa/use my
3646
+ * team/bot names). Its output drove legacy workflow step construction.
3647
+ * Codex audit HIGH #6: remove as an execution source. The orchestrator
3648
+ * LLM decides the ordered role list via create_workflow now; code
3649
+ * shouldn't infer "Ben -> John -> Ver" from English. Kept as a stub
3650
+ * until all legacy callers are removed; downstream paths already
3651
+ * handle an empty return by falling through to structured dispatch
3652
+ * or user-prompted clarification.
3653
+ */
3654
+ inferRequestedWorkflowRoles(_prompt, _roleAssignments) {
3655
+ return [];
2582
3656
  }
2583
3657
  orchestratorOwnsWorkflowRole(role, orchestratorBot, roleAssignments) {
2584
3658
  const orchestratorName = orchestratorBot.name.toLowerCase();
2585
3659
  const assignedName = roleAssignments[role]?.toLowerCase();
2586
3660
  if (assignedName)
2587
3661
  return assignedName === orchestratorName;
2588
- const roleClass = String(orchestratorBot.role_class || '').trim().toLowerCase();
2589
3662
  if (role === 'coding')
2590
- return ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager'].includes(roleClass);
3663
+ return this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager']);
2591
3664
  if (role === 'qa')
2592
- return ['qa', 'review', 'reviewer'].includes(roleClass);
3665
+ return this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']);
2593
3666
  if (role === 'research')
2594
- return ['research', 'researcher', 'analyst', 'advisor'].includes(roleClass);
3667
+ return this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design']);
2595
3668
  return false;
2596
3669
  }
2597
3670
  resolveWorkflowRoleTarget(role, orchestratorBot, roleAssignments) {
@@ -2687,7 +3760,7 @@ class OrchestratorAgent {
2687
3760
  'Do not build the final artifact in this step.',
2688
3761
  'Your deliverable is a concrete implementation direction document that the coding step can use immediately.',
2689
3762
  '',
2690
- `Original user request (reference only):\n${originalPrompt}`,
3763
+ `Original user request:\n${originalPrompt}`,
2691
3764
  artifactContext,
2692
3765
  '',
2693
3766
  'Your job: research the request, pressure-test the approach, and recommend the best implementation direction.',
@@ -2701,7 +3774,7 @@ class OrchestratorAgent {
2701
3774
  'Do not perform QA in this step.',
2702
3775
  'Your deliverable is the actual file or code change requested by the user.',
2703
3776
  '',
2704
- `Original user request (reference only):\n${originalPrompt}`,
3777
+ `Original user request:\n${originalPrompt}`,
2705
3778
  artifactContext,
2706
3779
  researchContext,
2707
3780
  qaContext,
@@ -2719,7 +3792,7 @@ class OrchestratorAgent {
2719
3792
  'You are the QA/review step in a multi-step workflow.',
2720
3793
  'Do not redo the coding or research work in this step unless the file is missing and you must report that blocker.',
2721
3794
  '',
2722
- `Original user request (reference only):\n${originalPrompt}`,
3795
+ `Original user request:\n${originalPrompt}`,
2723
3796
  artifactContext,
2724
3797
  researchContext,
2725
3798
  implementationContext,
@@ -2879,31 +3952,6 @@ class OrchestratorAgent {
2879
3952
  }
2880
3953
  return currentProjectId || intentProjectId;
2881
3954
  }
2882
- /**
2883
- * Handle simple requests — route to best agent, return response.
2884
- */
2885
- async handleSimpleRequest(prompt, conversationId, intent, opts) {
2886
- const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
2887
- if (agents.length === 0)
2888
- return 'No worker agents configured. Please add an agent in Settings.';
2889
- const route = await this.clerk.routeTask(prompt, agents);
2890
- this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Routing request to ${route.agentName}`);
2891
- this.publishOrchestratorStatus(opts, {
2892
- phase: 'delegating',
2893
- steps: [{ index: 0, description: prompt.slice(0, 80), agentName: route.agentName, status: 'running' }],
2894
- currentStep: 0,
2895
- totalSteps: 1,
2896
- });
2897
- // Execute via workflow engine — disable decomposition for simple mode
2898
- const result = await this.workflowEngine.execute(prompt, conversationId, route.agentId, {
2899
- apiKey: this.resolveApiKey(route.provider),
2900
- disableDecomposition: true,
2901
- onWorkerChunk: opts.onWorkerChunk,
2902
- });
2903
- this.publishOrchestratorStatus(opts, { phase: 'complete', totalSteps: 1, currentStep: 1 });
2904
- // Strip worker protocol before returning to user
2905
- return this.stripWorkerProtocol(result.mergedResult);
2906
- }
2907
3955
  /**
2908
3956
  * Handle proxy/discussion requests — forward to specific agent.
2909
3957
  */
@@ -2953,6 +4001,23 @@ class OrchestratorAgent {
2953
4001
  });
2954
4002
  }
2955
4003
  this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Connecting request to ${agent.name}`);
4004
+ const forwardProxyWorkerChunk = (event) => {
4005
+ if (!opts.onWorkerChunk)
4006
+ return;
4007
+ if (event.type === 'chunk') {
4008
+ opts.onWorkerChunk({ ...event, type: 'worker_chunk' });
4009
+ return;
4010
+ }
4011
+ if (event.type === 'tool_call') {
4012
+ opts.onWorkerChunk({ ...event, type: 'worker_tool_call' });
4013
+ return;
4014
+ }
4015
+ if (event.type === 'tool_result') {
4016
+ opts.onWorkerChunk({ ...event, type: 'worker_tool_result' });
4017
+ return;
4018
+ }
4019
+ opts.onWorkerChunk(event);
4020
+ };
2956
4021
  // Update conversation routing mode to 'proxy'
2957
4022
  try {
2958
4023
  const db = data.getDb();
@@ -2962,14 +4027,17 @@ class OrchestratorAgent {
2962
4027
  // Execute single step — disable decomposition for proxy mode
2963
4028
  const runProxyStep = (stepPrompt) => this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(stepPrompt, conversationId, agent.id, {
2964
4029
  apiKey: this.resolveApiKey(agent.provider),
4030
+ abortSignal: opts.abortSignal,
2965
4031
  disableDecomposition: true,
2966
- isOrchestrated: true,
4032
+ isOrchestrated: false,
2967
4033
  projectId: state?.projectId ?? null,
2968
4034
  workspacePath: state?.projectId
2969
4035
  ? data.getProject(state.projectId)?.folder?.trim() || undefined
2970
4036
  : undefined,
2971
4037
  persistConversationMessages: false,
2972
- onWorkerChunk: opts.onWorkerChunk,
4038
+ storedAttachments: opts.storedAttachments,
4039
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
4040
+ onWorkerChunk: forwardProxyWorkerChunk,
2973
4041
  onProgress: (progress) => {
2974
4042
  if (progress.event === 'step-started') {
2975
4043
  this.reportHiddenRoleProgress(opts, 'orchestrator', `${progress.step.agentName} is working on the delegated step`);
@@ -3129,15 +4197,25 @@ class OrchestratorAgent {
3129
4197
  if (state && workflowTemplate) {
3130
4198
  state.preferredWorkflowUsed = true;
3131
4199
  }
3132
- // TODO: planWorkflow uses the cheap clerk (GPT-4o-mini). Per architecture doc,
3133
- // workflow planning should be done by the orchestrator LLM, not the clerk.
3134
- // The clerk should only do summaries, extraction, and compression.
4200
+ const inferredWorkflowRoles = this.inferRequestedWorkflowRoles(prompt, roleAssignments);
3135
4201
  const plannedSteps = !workflowTemplate
3136
- ? await this.clerk.planWorkflow({
3137
- prompt,
3138
- agents,
3139
- roleAssignments,
3140
- }).catch(() => null)
4202
+ ? inferredWorkflowRoles.map((role, index) => {
4203
+ const ownerName = roleAssignments[role] || defaultAgent.name;
4204
+ return {
4205
+ description: role === 'research'
4206
+ ? 'Research and pressure-test the request'
4207
+ : role === 'qa'
4208
+ ? 'Review and QA the completed work'
4209
+ : 'Implement the requested work',
4210
+ agentName: ownerName,
4211
+ expectedOutput: role === 'qa'
4212
+ ? 'PASS if the work meets the request, otherwise FAIL with actionable defects'
4213
+ : role === 'research'
4214
+ ? 'Clear research findings and implementation guidance'
4215
+ : 'Completed implementation that satisfies the request',
4216
+ dependsOn: index > 0 ? [index - 1] : [],
4217
+ };
4218
+ })
3141
4219
  : null;
3142
4220
  let todoTaskId;
3143
4221
  if (executionSpec.allowTodoCreation) {
@@ -3227,17 +4305,22 @@ class OrchestratorAgent {
3227
4305
  coderRetryLimit: effectivePolicy.coderRetryLimit,
3228
4306
  workspacePath: effectiveWorkspacePath,
3229
4307
  projectId: project?.id || opts.projectId || null,
4308
+ storedAttachments: opts.storedAttachments,
4309
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
3230
4310
  onProgress: progressHandler,
3231
4311
  onWorkerChunk: opts.onWorkerChunk,
3232
4312
  })
3233
4313
  : this.workflowEngine.execute(prompt, conversationId, defaultAgent.id, {
3234
4314
  apiKey: this.resolveApiKey(defaultAgent.provider),
4315
+ abortSignal: opts.abortSignal,
3235
4316
  taskId: todoTaskId,
3236
4317
  isOrchestrated: true,
3237
4318
  roleAssignments,
3238
4319
  coderRetryLimit: effectivePolicy.coderRetryLimit,
3239
4320
  workspacePath: effectiveWorkspacePath,
3240
4321
  projectId: project?.id || opts.projectId || null,
4322
+ storedAttachments: opts.storedAttachments,
4323
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
3241
4324
  onProgress: progressHandler,
3242
4325
  onWorkerChunk: opts.onWorkerChunk,
3243
4326
  }));
@@ -3307,6 +4390,36 @@ class OrchestratorAgent {
3307
4390
  }
3308
4391
  return lines.join('\n');
3309
4392
  }
4393
+ /**
4394
+ * Run completion validation on a synthesized response and append warnings
4395
+ * if the validation finds error-severity issues. Non-blocking — always
4396
+ * returns a response, possibly with a validation warning appended.
4397
+ */
4398
+ applyCompletionValidation(synthesized, result, conversationId, executionMode) {
4399
+ try {
4400
+ const auditMessages = conversationId
4401
+ ? data.getMessages(conversationId, { limit: 200 }).map((m) => ({
4402
+ role: m.role,
4403
+ content: m.content,
4404
+ tool_calls_json: m.tool_calls_json,
4405
+ }))
4406
+ : [];
4407
+ const validation = (0, validation_1.validateCompletion)(result, synthesized, auditMessages, executionMode);
4408
+ if (!validation.valid) {
4409
+ console.warn(`[orchestrator] Completion validation failed (confidence=${validation.confidence}):`, JSON.stringify(validation.issues));
4410
+ return synthesized + (0, validation_1.formatValidationWarning)(validation);
4411
+ }
4412
+ if (validation.issues.length > 0) {
4413
+ console.info(`[orchestrator] Completion validation passed with warnings (confidence=${validation.confidence}):`, JSON.stringify(validation.issues));
4414
+ }
4415
+ return synthesized;
4416
+ }
4417
+ catch (err) {
4418
+ // Validation must never block the response
4419
+ console.warn('[orchestrator] Completion validation error (non-blocking):', err);
4420
+ return synthesized;
4421
+ }
4422
+ }
3310
4423
  /**
3311
4424
  * Publish orchestrator status via MQTT for UI rendering.
3312
4425
  */
@@ -3356,6 +4469,83 @@ class OrchestratorAgent {
3356
4469
  reportHiddenRoleProgress(opts, roleName, detail) {
3357
4470
  opts.onProgress(`${roleName}::${detail}`);
3358
4471
  }
4472
+ /**
4473
+ * Returns a callback that forwards orchestrator narration chunks to the
4474
+ * desktop as 'chunk' WorkerChunkEvents tagged with the orchestrator bot's identity.
4475
+ * This is how the front-door narration appears live in the orchestrator card.
4476
+ */
4477
+ makeOrchestratorNarrationForwarder(opts, orchestratorBot) {
4478
+ if (!opts.onWorkerChunk)
4479
+ return undefined;
4480
+ const stepId = `orchestrator-narration-${orchestratorBot.id}`;
4481
+ return (text) => {
4482
+ if (!text)
4483
+ return;
4484
+ try {
4485
+ opts.onWorkerChunk?.({
4486
+ type: 'chunk',
4487
+ stepId,
4488
+ agentName: 'Orchestrator',
4489
+ description: 'Orchestrator narration',
4490
+ stepIndex: -1,
4491
+ totalSteps: 0,
4492
+ text,
4493
+ });
4494
+ }
4495
+ catch {
4496
+ // best-effort streaming
4497
+ }
4498
+ };
4499
+ }
4500
+ /**
4501
+ * Synthesizes a short factual narration sentence when the LLM front-door call
4502
+ * failed entirely and Funolio fell back to a system suggestion. Never returns
4503
+ * robotic boilerplate — uses real bot/role names whenever available.
4504
+ */
4505
+ synthesizeFallbackNarration(decision, roleAssignments) {
4506
+ const mode = decision?.mode;
4507
+ if (!mode)
4508
+ return null;
4509
+ if (mode === 'delegate') {
4510
+ const target = decision.delegate_target?.trim();
4511
+ if (target && target.toUpperCase() !== 'NONE') {
4512
+ return `Routing this to ${target}.`;
4513
+ }
4514
+ return 'Routing this to the appropriate worker.';
4515
+ }
4516
+ if (mode === 'workflow') {
4517
+ // Use real worker names from assignments if available.
4518
+ const workerNames = [];
4519
+ if (roleAssignments) {
4520
+ if (roleAssignments.research)
4521
+ workerNames.push(roleAssignments.research);
4522
+ if (roleAssignments.coding)
4523
+ workerNames.push(roleAssignments.coding);
4524
+ if (roleAssignments.qa)
4525
+ workerNames.push(roleAssignments.qa);
4526
+ }
4527
+ const uniqueNames = Array.from(new Set(workerNames.map((n) => String(n || '').trim()).filter(Boolean)));
4528
+ if (uniqueNames.length === 0) {
4529
+ return 'Setting up a multi-step workflow for this.';
4530
+ }
4531
+ if (uniqueNames.length === 1) {
4532
+ return `Handing this to ${uniqueNames[0]} as a workflow.`;
4533
+ }
4534
+ if (uniqueNames.length === 2) {
4535
+ return `Setting up a workflow with ${uniqueNames[0]} and ${uniqueNames[1]}.`;
4536
+ }
4537
+ const last = uniqueNames[uniqueNames.length - 1];
4538
+ const front = uniqueNames.slice(0, -1).join(', ');
4539
+ return `Setting up a workflow with ${front}, and ${last}.`;
4540
+ }
4541
+ if (mode === 'clarify') {
4542
+ return 'I need a quick clarification before I can route this.';
4543
+ }
4544
+ if (mode === 'execute_self') {
4545
+ return 'Working on this now.';
4546
+ }
4547
+ return null;
4548
+ }
3359
4549
  formatConfirmationRequest(prompt) {
3360
4550
  const singleLine = String(prompt || '').replace(/\s+/g, ' ').trim();
3361
4551
  if (!singleLine)
@@ -3474,76 +4664,129 @@ class OrchestratorAgent {
3474
4664
  }
3475
4665
  return assignments;
3476
4666
  }
3477
- clerkModelLabel() {
3478
- const runtime = this.clerk.getRuntimeInfo();
3479
- if (!runtime.model)
3480
- return 'Clerk';
3481
- return `${runtime.model} | Clerk`;
3482
- }
3483
- async respondAsClerk(userPrompt, systemPrompt) {
3484
- const response = await this.clerk.respond(userPrompt, systemPrompt);
3485
- this.setLastResponseMeta({
3486
- agentName: 'Clerk',
3487
- botId: null,
3488
- modelLabel: this.clerkModelLabel(),
3489
- });
3490
- return response;
3491
- }
3492
4667
  hasRoleAssignments(assignments) {
3493
4668
  return Boolean(assignments.coding || assignments.qa || assignments.research);
3494
4669
  }
3495
- loadRoleAssignments(projectId) {
3496
- const raw = data.getProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY);
3497
- if (!raw)
3498
- return {};
3499
- try {
3500
- const parsed = JSON.parse(raw);
3501
- return {
3502
- coding: parsed.coding || undefined,
3503
- qa: parsed.qa || undefined,
3504
- research: parsed.research || undefined,
3505
- };
3506
- }
3507
- catch {
3508
- return {};
3509
- }
3510
- }
3511
- mergeRoleAssignments(base, saved, inferred, incoming) {
4670
+ mergeRoleAssignments(base, inferred, incoming) {
3512
4671
  return {
3513
- coding: incoming.coding || saved.coding || base.coding || inferred.coding,
3514
- qa: incoming.qa || saved.qa || base.qa || inferred.qa,
3515
- research: incoming.research || saved.research || base.research || inferred.research,
4672
+ coding: incoming.coding || base.coding || inferred.coding,
4673
+ qa: incoming.qa || base.qa || inferred.qa,
4674
+ research: incoming.research || base.research || inferred.research,
3516
4675
  };
3517
4676
  }
3518
4677
  deriveRoleAssignmentsFromProject(projectId, orchestratorBot) {
3519
4678
  const bots = this.listProjectScopedBots(projectId);
3520
4679
  const result = {};
3521
- const roleClass = String(orchestratorBot?.role_class || '').trim().toLowerCase();
3522
4680
  if (orchestratorBot?.name) {
3523
- if (['code', 'coding', 'coder', 'builder', 'developer', 'engineer'].includes(roleClass)) {
4681
+ if (this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])) {
3524
4682
  result.coding = orchestratorBot.name;
3525
4683
  }
3526
- else if (['qa', 'review', 'reviewer'].includes(roleClass)) {
4684
+ else if (this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer'])) {
3527
4685
  result.qa = orchestratorBot.name;
3528
4686
  }
3529
- else if (['research', 'researcher', 'analyst', 'advisor'].includes(roleClass)) {
4687
+ else if (this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])) {
3530
4688
  result.research = orchestratorBot.name;
3531
4689
  }
3532
4690
  }
3533
- const findByRole = (roles) => bots.find((bot) => roles.includes(String(bot.role_class || '').trim().toLowerCase()));
4691
+ const findByRole = (roles) => bots.find((bot) => this.agentHasAnyRole(bot, roles));
3534
4692
  result.coding = result.coding || findByRole(['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])?.name;
3535
4693
  result.qa = result.qa || findByRole(['qa', 'review', 'reviewer'])?.name;
3536
- result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor'])?.name;
4694
+ result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])?.name;
3537
4695
  return result;
3538
4696
  }
3539
- listProjectScopedBots(projectId) {
4697
+ listProjectScopedBots(projectId, opts) {
4698
+ const maybeFilterOrchestrators = (bots) => opts?.includeOrchestratorProfiles ? bots : (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(bots);
3540
4699
  if (!projectId)
3541
- return (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
4700
+ return maybeFilterOrchestrators(data.listAgentProfiles());
3542
4701
  const project = data.getProject(projectId);
3543
4702
  const botIds = new Set(project?.bot_ids || []);
3544
4703
  const allBots = data.listAgentProfiles();
3545
4704
  const scoped = allBots.filter((bot) => botIds.has(bot.id));
3546
- return (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(scoped.length > 0 ? scoped : allBots);
4705
+ return maybeFilterOrchestrators(scoped.length > 0 ? scoped : allBots);
4706
+ }
4707
+ resolvePlanImportBot(plannerBotId, projectId, orchestratorBot) {
4708
+ const candidates = this.listProjectScopedBots(projectId, { includeOrchestratorProfiles: true })
4709
+ .filter((bot) => bot.is_active === 1);
4710
+ const orchestratorCandidate = orchestratorBot && orchestratorBot.is_active === 1
4711
+ ? orchestratorBot
4712
+ : null;
4713
+ const pool = orchestratorCandidate && !candidates.some((bot) => bot.id === orchestratorCandidate.id)
4714
+ ? [...candidates, orchestratorCandidate]
4715
+ : candidates;
4716
+ const selected = pool.find((bot) => bot.id === plannerBotId);
4717
+ if (!selected) {
4718
+ throw new Error('Choose an active planner bot for this project.');
4719
+ }
4720
+ return selected;
4721
+ }
4722
+ async runPlanImportPlannerTurn(input) {
4723
+ const syntheticConversationId = `${input.conversationId}::plan-import`;
4724
+ if (input.plannerBot.provider === 'claude-cli') {
4725
+ const manager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
4726
+ try {
4727
+ const result = await manager.runTurn({
4728
+ conversationId: syntheticConversationId,
4729
+ botId: input.plannerBot.id,
4730
+ provider: 'claude-cli',
4731
+ botSettings: {
4732
+ claude: {
4733
+ model: input.plannerBot.model,
4734
+ effortLevel: input.plannerBot.claude_effort_level,
4735
+ outputStyle: input.plannerBot.claude_output_style,
4736
+ fastMode: input.plannerBot.claude_fast_mode === 1,
4737
+ permissionsJson: input.plannerBot.claude_permissions_json,
4738
+ },
4739
+ },
4740
+ cwd: input.workspacePath,
4741
+ systemPrompt: input.systemPrompt,
4742
+ messages: input.messages,
4743
+ forceFreshSession: true,
4744
+ newSessionId: data.generateNextSessionId(),
4745
+ abortSignal: input.abortSignal,
4746
+ });
4747
+ return String(result.content || '').trim();
4748
+ }
4749
+ finally {
4750
+ manager.closeSessionByConversation(syntheticConversationId, input.plannerBot.id);
4751
+ }
4752
+ }
4753
+ if (input.plannerBot.provider === 'codex-cli' && !(0, storage_mode_1.isServerStorageMode)()) {
4754
+ const manager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
4755
+ try {
4756
+ const result = await manager.runTurn({
4757
+ runtimeMode: 'local_desktop',
4758
+ conversationId: syntheticConversationId,
4759
+ botId: input.plannerBot.id,
4760
+ botName: input.plannerBot.name,
4761
+ cwd: input.workspacePath,
4762
+ systemPrompt: input.systemPrompt,
4763
+ messages: input.messages,
4764
+ forceFreshSession: true,
4765
+ model: input.plannerBot.model || null,
4766
+ projectId: input.projectId,
4767
+ codexSettings: {
4768
+ reasoningEffort: input.plannerBot.codex_reasoning_effort,
4769
+ reasoningSummary: input.plannerBot.codex_reasoning_summary,
4770
+ personality: input.plannerBot.codex_personality,
4771
+ serviceTier: input.plannerBot.codex_service_tier,
4772
+ sandboxPolicy: input.plannerBot.codex_sandbox_policy,
4773
+ approvalPolicy: input.plannerBot.codex_approval_policy,
4774
+ },
4775
+ abortSignal: input.abortSignal,
4776
+ });
4777
+ return String(result.content || '').trim();
4778
+ }
4779
+ finally {
4780
+ manager.closeSessionByConversation(syntheticConversationId, input.plannerBot.id);
4781
+ }
4782
+ }
4783
+ const response = await input.plannerRuntime.llm.chat({
4784
+ system: input.systemPrompt,
4785
+ messages: input.messages,
4786
+ maxOutputTokens: 20_000,
4787
+ abortSignal: input.abortSignal,
4788
+ });
4789
+ return String(response?.content || '').trim();
3547
4790
  }
3548
4791
  policyRoleAssignments(policy) {
3549
4792
  return {
@@ -3670,128 +4913,14 @@ class OrchestratorAgent {
3670
4913
  isPolicyConfirmation(prompt) {
3671
4914
  return /\b(yes|yes save it|save it|save this policy|apply it|apply this|confirm|confirmed|do that|sounds right)\b/i.test(prompt.trim());
3672
4915
  }
3673
- determinePolicyScope(prompt, projectId) {
3674
- if (!projectId)
3675
- return 'user';
3676
- if (/\b(for|in|on|within)\s+(this|the)\s+project\b/i.test(prompt))
3677
- return 'project';
3678
- const project = data.getProject(projectId);
3679
- if (project && new RegExp(`\\b${project.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(prompt)) {
3680
- return 'project';
3681
- }
3682
- return 'user';
3683
- }
3684
- extractPolicyPatch(prompt, assignments, currentPolicy) {
3685
- const patch = {};
3686
- if (/\b(?:you|o|orchestrator|project manager)\s+never\s+code(?:s)?\b|\bnot code\b/i.test(prompt))
3687
- patch.allowOrchestratorCode = false;
3688
- if (/\b(?:you|o|orchestrator|project manager)\s+never\s+qa\b|\bnot qa\b/i.test(prompt))
3689
- patch.allowOrchestratorQa = false;
3690
- if (/\b(?:you|o|orchestrator|project manager)\s+never\s+deploy(?:s)?\b|\bnot deploy\b/i.test(prompt))
3691
- patch.allowOrchestratorDeploy = false;
3692
- if (/\b(?:you|o|orchestrator|project manager)\s+never\s+simulate(?:s)?\b|\bdo not simulate\b|\bnot simulate\b/i.test(prompt))
3693
- patch.allowOrchestratorSimulation = false;
3694
- if (/\bcasual\b/i.test(prompt) && /\bdirect\b/i.test(prompt))
3695
- patch.tone = 'casual_direct';
3696
- else if (/\bcasual\b/i.test(prompt))
3697
- patch.tone = 'casual';
3698
- else if (/\bdirect\b/i.test(prompt))
3699
- patch.tone = 'direct';
3700
- if (/\bstrict\b/i.test(prompt))
3701
- patch.policyEnforcementLevel = 'strict';
3702
- if (/\bno more than (\d+)\s+times\b/i.test(prompt)) {
3703
- patch.coderRetryLimit = Number(prompt.match(/\bno more than (\d+)\s+times\b/i)?.[1] || currentPolicy.coderRetryLimit || 2);
3704
- }
3705
- if (/\b30\s*seconds\b/i.test(prompt))
3706
- patch.statusReportingMode = 'event_plus_heartbeat';
3707
- if (assignments.research)
3708
- patch.defaultIdeaReviewer = assignments.research;
3709
- if (assignments.coding)
3710
- patch.defaultCoder = assignments.coding;
3711
- if (assignments.qa)
3712
- patch.defaultQa = assignments.qa;
3713
- return patch;
3714
- }
3715
- sanitizeInterpretedPolicyPatch(prompt, patch) {
3716
- const sanitized = {};
3717
- const normalized = prompt.toLowerCase();
3718
- const mentionsCodePolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\b(code|codes|coding)\b/.test(normalized);
3719
- const mentionsQaPolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\b(qa|review|test|testing)\b/.test(normalized);
3720
- const mentionsDeployPolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\bdeploy(?:s|ing)?\b/.test(normalized);
3721
- const mentionsSimulationPolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\bsimulat(?:e|es|ing|ion)\b/.test(normalized);
3722
- if (patch.allowOrchestratorCode !== undefined && mentionsCodePolicy)
3723
- sanitized.allowOrchestratorCode = patch.allowOrchestratorCode;
3724
- if (patch.allowOrchestratorQa !== undefined && mentionsQaPolicy)
3725
- sanitized.allowOrchestratorQa = patch.allowOrchestratorQa;
3726
- if (patch.allowOrchestratorDeploy !== undefined && mentionsDeployPolicy)
3727
- sanitized.allowOrchestratorDeploy = patch.allowOrchestratorDeploy;
3728
- if (patch.allowOrchestratorSimulation !== undefined && mentionsSimulationPolicy)
3729
- sanitized.allowOrchestratorSimulation = patch.allowOrchestratorSimulation;
3730
- if (patch.defaultIdeaReviewer)
3731
- sanitized.defaultIdeaReviewer = patch.defaultIdeaReviewer;
3732
- if (patch.defaultCoder)
3733
- sanitized.defaultCoder = patch.defaultCoder;
3734
- if (patch.defaultBackendCoder)
3735
- sanitized.defaultBackendCoder = patch.defaultBackendCoder;
3736
- if (patch.defaultQa)
3737
- sanitized.defaultQa = patch.defaultQa;
3738
- if (patch.coderRetryLimit !== undefined)
3739
- sanitized.coderRetryLimit = patch.coderRetryLimit;
3740
- if (patch.statusReportingMode)
3741
- sanitized.statusReportingMode = patch.statusReportingMode;
3742
- if (patch.tone)
3743
- sanitized.tone = patch.tone;
3744
- if (patch.policyEnforcementLevel)
3745
- sanitized.policyEnforcementLevel = patch.policyEnforcementLevel;
3746
- return sanitized;
3747
- }
3748
- summarizePolicyPatch(patch) {
3749
- const lines = [];
3750
- if (patch.allowOrchestratorCode === false)
3751
- lines.push('O should not code');
3752
- if (patch.allowOrchestratorQa === false)
3753
- lines.push('O should not QA');
3754
- if (patch.allowOrchestratorDeploy === false)
3755
- lines.push('O should not deploy');
3756
- if (patch.allowOrchestratorSimulation === false)
3757
- lines.push('O should not simulate work');
3758
- if (patch.defaultIdeaReviewer)
3759
- lines.push(`${patch.defaultIdeaReviewer} should review ideas by default`);
3760
- if (patch.defaultCoder)
3761
- lines.push(`${patch.defaultCoder} should code by default`);
3762
- if (patch.defaultBackendCoder)
3763
- lines.push(`${patch.defaultBackendCoder} should handle backend coding by default`);
3764
- if (patch.defaultQa)
3765
- lines.push(`${patch.defaultQa} should QA by default`);
3766
- if (patch.coderRetryLimit !== undefined)
3767
- lines.push(`Coder retry limit should be ${patch.coderRetryLimit}`);
3768
- if (patch.statusReportingMode)
3769
- lines.push(`Status reporting mode should be ${patch.statusReportingMode}`);
3770
- if (patch.tone)
3771
- lines.push(`Tone should be ${patch.tone.replace(/_/g, ' ')}`);
3772
- if (patch.policyEnforcementLevel)
3773
- lines.push(`Enforcement should be ${patch.policyEnforcementLevel}`);
3774
- return lines;
3775
- }
3776
4916
  async handlePolicyUpdate(prompt, conversationId, intent, assignments, projectId, overview, currentPolicy) {
3777
- const interpreted = await this.clerk.interpretPolicyUpdate({
3778
- prompt,
3779
- currentPolicy,
3780
- projectName: overview?.project.name,
3781
- }).catch(() => null);
3782
4917
  const agentNames = data.listAgentProfiles().map((agent) => agent.name);
3783
- const sanitizedInterpretedPatch = (0, policy_detection_1.sanitizePolicyPatch)(prompt, interpreted?.patch || {}, currentPolicy);
3784
- const patch = {
3785
- ...(0, policy_detection_1.extractPolicyPatchFromPrompt)(prompt, currentPolicy, { agentNames }),
3786
- ...sanitizedInterpretedPatch,
3787
- };
3788
- const changes = interpreted?.summaryLines?.length
3789
- ? interpreted.summaryLines
3790
- : (0, policy_detection_1.summarizePolicyPatch)(patch);
4918
+ const patch = (0, policy_detection_1.extractPolicyPatchFromPrompt)(prompt, currentPolicy, { agentNames });
4919
+ const changes = (0, policy_detection_1.summarizePolicyPatch)(patch);
3791
4920
  if (changes.length === 0) {
3792
4921
  return this.formatPolicyUpdateResponse(prompt, intent, assignments, overview);
3793
4922
  }
3794
- const scope = interpreted?.scope || (0, policy_detection_1.determinePolicyScope)(prompt, overview?.project.name || undefined);
4923
+ const scope = (0, policy_detection_1.determinePolicyScope)(prompt, overview?.project.name || undefined);
3795
4924
  data.setPendingOrchestrationPolicy({
3796
4925
  conversationId,
3797
4926
  projectId: scope === 'project' ? projectId || null : null,
@@ -3862,35 +4991,6 @@ class OrchestratorAgent {
3862
4991
  }
3863
4992
  return undefined;
3864
4993
  }
3865
- persistRoleAssignments(projectId, conversationId, agentId, assignments) {
3866
- const merged = this.mergeRoleAssignments({}, this.loadRoleAssignments(projectId), this.deriveRoleAssignmentsFromProject(projectId), assignments);
3867
- data.setProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY, JSON.stringify(merged));
3868
- const parts = [
3869
- merged.coding ? `${merged.coding} handles coding` : null,
3870
- merged.qa ? `${merged.qa} handles QA` : null,
3871
- merged.research ? `${merged.research} handles research` : null,
3872
- ].filter(Boolean);
3873
- if (parts.length > 0 && (0, storage_mode_1.isServerStorageMode)()) {
3874
- data.upsertMemoryFact({
3875
- agentId,
3876
- conversationId,
3877
- factType: 'operating_instruction',
3878
- content: `Default orchestrator assignments: ${parts.join('; ')}.`,
3879
- extractionMethod: 'orchestrator',
3880
- });
3881
- }
3882
- data.logAdminAudit({
3883
- actorType: 'orchestrator',
3884
- actorId: 'Project Manager',
3885
- projectId,
3886
- resourceType: 'project_setting',
3887
- resourceId: `${projectId}:${ORCHESTRATOR_ROLE_SETTING_KEY}`,
3888
- action: 'set_orchestrator_role_assignments',
3889
- request: assignments,
3890
- after: merged,
3891
- result: 'ok',
3892
- });
3893
- }
3894
4994
  roleAssignmentParticipants(assignments, agents) {
3895
4995
  return Array.from(new Set([assignments.coding, assignments.qa, assignments.research]
3896
4996
  .filter((name) => Boolean(name))