funolio-agent 1.0.75 → 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 (236) hide show
  1. package/dist/auth/credential-reader.d.ts.map +1 -1
  2. package/dist/auth/credential-reader.js +4 -3
  3. package/dist/auth/credential-reader.js.map +1 -1
  4. package/dist/auth/token-refresh.d.ts +8 -0
  5. package/dist/auth/token-refresh.d.ts.map +1 -1
  6. package/dist/auth/token-refresh.js +82 -52
  7. package/dist/auth/token-refresh.js.map +1 -1
  8. package/dist/auto-organizer.d.ts.map +1 -1
  9. package/dist/auto-organizer.js +6 -7
  10. package/dist/auto-organizer.js.map +1 -1
  11. package/dist/bench-prefix.d.ts +16 -0
  12. package/dist/bench-prefix.d.ts.map +1 -0
  13. package/dist/bench-prefix.js +25 -0
  14. package/dist/bench-prefix.js.map +1 -0
  15. package/dist/bot-manager.d.ts.map +1 -1
  16. package/dist/bot-manager.js +23 -14
  17. package/dist/bot-manager.js.map +1 -1
  18. package/dist/chat-sync.d.ts +42 -0
  19. package/dist/chat-sync.d.ts.map +1 -0
  20. package/dist/chat-sync.js +95 -0
  21. package/dist/chat-sync.js.map +1 -0
  22. package/dist/clerk-model.d.ts +7 -0
  23. package/dist/clerk-model.d.ts.map +1 -1
  24. package/dist/clerk-model.js +42 -8
  25. package/dist/clerk-model.js.map +1 -1
  26. package/dist/cli-bootstrap-history.d.ts +10 -0
  27. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  28. package/dist/cli-bootstrap-history.js +112 -0
  29. package/dist/cli-bootstrap-history.js.map +1 -0
  30. package/dist/cli-models.d.ts +8 -0
  31. package/dist/cli-models.d.ts.map +1 -0
  32. package/dist/cli-models.js +91 -0
  33. package/dist/cli-models.js.map +1 -0
  34. package/dist/cli-session-epoch.d.ts +13 -3
  35. package/dist/cli-session-epoch.d.ts.map +1 -1
  36. package/dist/cli-session-epoch.js +53 -4
  37. package/dist/cli-session-epoch.js.map +1 -1
  38. package/dist/codex-app-server-manager.d.ts +64 -4
  39. package/dist/codex-app-server-manager.d.ts.map +1 -1
  40. package/dist/codex-app-server-manager.js +755 -55
  41. package/dist/codex-app-server-manager.js.map +1 -1
  42. package/dist/commands/pool.d.ts +32 -0
  43. package/dist/commands/pool.d.ts.map +1 -1
  44. package/dist/commands/pool.js +145 -66
  45. package/dist/commands/pool.js.map +1 -1
  46. package/dist/commands/start.d.ts +21 -0
  47. package/dist/commands/start.d.ts.map +1 -1
  48. package/dist/commands/start.js +484 -63
  49. package/dist/commands/start.js.map +1 -1
  50. package/dist/commands/status.d.ts.map +1 -1
  51. package/dist/commands/status.js +5 -2
  52. package/dist/commands/status.js.map +1 -1
  53. package/dist/config.d.ts +1 -0
  54. package/dist/config.d.ts.map +1 -1
  55. package/dist/config.js +170 -58
  56. package/dist/config.js.map +1 -1
  57. package/dist/context-window.d.ts +37 -1
  58. package/dist/context-window.d.ts.map +1 -1
  59. package/dist/context-window.js +202 -16
  60. package/dist/context-window.js.map +1 -1
  61. package/dist/live-activity.d.ts +3 -1
  62. package/dist/live-activity.d.ts.map +1 -1
  63. package/dist/live-activity.js.map +1 -1
  64. package/dist/local-chat-execution.d.ts +114 -0
  65. package/dist/local-chat-execution.d.ts.map +1 -0
  66. package/dist/local-chat-execution.js +349 -0
  67. package/dist/local-chat-execution.js.map +1 -0
  68. package/dist/local-cli-pty-manager.d.ts +138 -3
  69. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  70. package/dist/local-cli-pty-manager.js +1415 -111
  71. package/dist/local-cli-pty-manager.js.map +1 -1
  72. package/dist/local-conversation-gateway.d.ts +110 -0
  73. package/dist/local-conversation-gateway.d.ts.map +1 -0
  74. package/dist/local-conversation-gateway.js +175 -0
  75. package/dist/local-conversation-gateway.js.map +1 -0
  76. package/dist/local-data.d.ts +235 -5
  77. package/dist/local-data.d.ts.map +1 -1
  78. package/dist/local-data.js +1066 -87
  79. package/dist/local-data.js.map +1 -1
  80. package/dist/local-db.d.ts +6 -0
  81. package/dist/local-db.d.ts.map +1 -1
  82. package/dist/local-db.js +376 -4
  83. package/dist/local-db.js.map +1 -1
  84. package/dist/local-funnel.d.ts.map +1 -1
  85. package/dist/local-funnel.js +6 -5
  86. package/dist/local-funnel.js.map +1 -1
  87. package/dist/local-server.d.ts +30 -0
  88. package/dist/local-server.d.ts.map +1 -1
  89. package/dist/local-server.js +2898 -319
  90. package/dist/local-server.js.map +1 -1
  91. package/dist/managed-process-registry.d.ts +59 -0
  92. package/dist/managed-process-registry.d.ts.map +1 -0
  93. package/dist/managed-process-registry.js +390 -0
  94. package/dist/managed-process-registry.js.map +1 -0
  95. package/dist/mcp/claude-config-writer.d.ts +5 -5
  96. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  97. package/dist/mcp/claude-config-writer.js +19 -11
  98. package/dist/mcp/claude-config-writer.js.map +1 -1
  99. package/dist/mcp/index.d.ts +4 -2
  100. package/dist/mcp/index.d.ts.map +1 -1
  101. package/dist/mcp/index.js.map +1 -1
  102. package/dist/mcp/sync-cli-config.d.ts +42 -4
  103. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  104. package/dist/mcp/sync-cli-config.js +497 -17
  105. package/dist/mcp/sync-cli-config.js.map +1 -1
  106. package/dist/message-loop.d.ts.map +1 -1
  107. package/dist/message-loop.js +43 -1
  108. package/dist/message-loop.js.map +1 -1
  109. package/dist/mqtt-client.d.ts +34 -0
  110. package/dist/mqtt-client.d.ts.map +1 -1
  111. package/dist/mqtt-client.js +270 -45
  112. package/dist/mqtt-client.js.map +1 -1
  113. package/dist/mqtt-data-relay.d.ts +44 -0
  114. package/dist/mqtt-data-relay.d.ts.map +1 -0
  115. package/dist/mqtt-data-relay.js +106 -0
  116. package/dist/mqtt-data-relay.js.map +1 -0
  117. package/dist/orchestration/capabilities.d.ts +13 -0
  118. package/dist/orchestration/capabilities.d.ts.map +1 -0
  119. package/dist/orchestration/capabilities.js +152 -0
  120. package/dist/orchestration/capabilities.js.map +1 -0
  121. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  122. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  123. package/dist/orchestration/dispatch-executor.js +266 -0
  124. package/dist/orchestration/dispatch-executor.js.map +1 -0
  125. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  126. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  127. package/dist/orchestration/dispatch-hint.js +247 -0
  128. package/dist/orchestration/dispatch-hint.js.map +1 -0
  129. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  130. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  131. package/dist/orchestration/dispatch-runner.js +604 -0
  132. package/dist/orchestration/dispatch-runner.js.map +1 -0
  133. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  134. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  135. package/dist/orchestration/dispatch-tools.js +328 -0
  136. package/dist/orchestration/dispatch-tools.js.map +1 -0
  137. package/dist/orchestration/front-door-policy.d.ts +35 -10
  138. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  139. package/dist/orchestration/front-door-policy.js +30 -267
  140. package/dist/orchestration/front-door-policy.js.map +1 -1
  141. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  142. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  143. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  144. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  145. package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
  146. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  147. package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
  148. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  149. package/dist/orchestration/plan-import.d.ts +39 -0
  150. package/dist/orchestration/plan-import.d.ts.map +1 -0
  151. package/dist/orchestration/plan-import.js +547 -0
  152. package/dist/orchestration/plan-import.js.map +1 -0
  153. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  154. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  155. package/dist/orchestration/worker-operating-prompt.js +36 -46
  156. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  157. package/dist/orchestrator.d.ts +195 -3
  158. package/dist/orchestrator.d.ts.map +1 -1
  159. package/dist/orchestrator.js +1970 -432
  160. package/dist/orchestrator.js.map +1 -1
  161. package/dist/providers/anthropic.d.ts.map +1 -1
  162. package/dist/providers/anthropic.js +8 -4
  163. package/dist/providers/anthropic.js.map +1 -1
  164. package/dist/providers/claude-cli.d.ts.map +1 -1
  165. package/dist/providers/claude-cli.js +28 -3
  166. package/dist/providers/claude-cli.js.map +1 -1
  167. package/dist/providers/codex-cli.d.ts +10 -6
  168. package/dist/providers/codex-cli.d.ts.map +1 -1
  169. package/dist/providers/codex-cli.js +190 -17
  170. package/dist/providers/codex-cli.js.map +1 -1
  171. package/dist/providers/google.d.ts.map +1 -1
  172. package/dist/providers/google.js +15 -5
  173. package/dist/providers/google.js.map +1 -1
  174. package/dist/providers/index.d.ts +15 -1
  175. package/dist/providers/index.d.ts.map +1 -1
  176. package/dist/providers/index.js.map +1 -1
  177. package/dist/providers/openai.d.ts +1 -1
  178. package/dist/providers/openai.d.ts.map +1 -1
  179. package/dist/providers/openai.js +13 -5
  180. package/dist/providers/openai.js.map +1 -1
  181. package/dist/server-adapter.d.ts +8 -0
  182. package/dist/server-adapter.d.ts.map +1 -1
  183. package/dist/server-adapter.js +7 -0
  184. package/dist/server-adapter.js.map +1 -1
  185. package/dist/service-mode.d.ts +1 -1
  186. package/dist/service-mode.d.ts.map +1 -1
  187. package/dist/service-mode.js +64 -1
  188. package/dist/service-mode.js.map +1 -1
  189. package/dist/service-setup-only.d.ts +8 -0
  190. package/dist/service-setup-only.d.ts.map +1 -0
  191. package/dist/service-setup-only.js +37 -0
  192. package/dist/service-setup-only.js.map +1 -0
  193. package/dist/slash-commands.d.ts +21 -0
  194. package/dist/slash-commands.d.ts.map +1 -0
  195. package/dist/slash-commands.js +99 -0
  196. package/dist/slash-commands.js.map +1 -0
  197. package/dist/subagent/index.d.ts +4 -2
  198. package/dist/subagent/index.d.ts.map +1 -1
  199. package/dist/subagent/index.js.map +1 -1
  200. package/dist/summarization-pipeline.d.ts.map +1 -1
  201. package/dist/summarization-pipeline.js +1 -9
  202. package/dist/summarization-pipeline.js.map +1 -1
  203. package/dist/token-counter.d.ts.map +1 -1
  204. package/dist/token-counter.js +11 -4
  205. package/dist/token-counter.js.map +1 -1
  206. package/dist/tool-filter.d.ts.map +1 -1
  207. package/dist/tool-filter.js +10 -6
  208. package/dist/tool-filter.js.map +1 -1
  209. package/dist/tools/admin-tools.d.ts.map +1 -1
  210. package/dist/tools/admin-tools.js +13 -4
  211. package/dist/tools/admin-tools.js.map +1 -1
  212. package/dist/tools/run-command.d.ts.map +1 -1
  213. package/dist/tools/run-command.js +5 -1
  214. package/dist/tools/run-command.js.map +1 -1
  215. package/dist/tools/search-conversation-history.d.ts.map +1 -1
  216. package/dist/tools/search-conversation-history.js +12 -2
  217. package/dist/tools/search-conversation-history.js.map +1 -1
  218. package/dist/tools/todo-tasks.d.ts.map +1 -1
  219. package/dist/tools/todo-tasks.js +77 -5
  220. package/dist/tools/todo-tasks.js.map +1 -1
  221. package/dist/usage-log.d.ts +62 -0
  222. package/dist/usage-log.d.ts.map +1 -0
  223. package/dist/usage-log.js +98 -0
  224. package/dist/usage-log.js.map +1 -0
  225. package/dist/wizard-state.d.ts +13 -0
  226. package/dist/wizard-state.d.ts.map +1 -1
  227. package/dist/wizard-state.js +61 -3
  228. package/dist/wizard-state.js.map +1 -1
  229. package/dist/wizard-support.d.ts.map +1 -1
  230. package/dist/wizard-support.js +27 -1
  231. package/dist/wizard-support.js.map +1 -1
  232. package/dist/workflow-engine.d.ts +40 -1
  233. package/dist/workflow-engine.d.ts.map +1 -1
  234. package/dist/workflow-engine.js +753 -93
  235. package/dist/workflow-engine.js.map +1 -1
  236. package/package.json +2 -2
@@ -48,28 +48,36 @@ var __importStar = (this && this.__importStar) || (function () {
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.OrchestratorAgent = void 0;
50
50
  exports.buildLocalDesktopOrchestratorRuntime = buildLocalDesktopOrchestratorRuntime;
51
+ exports.buildNarrationStreamGate = buildNarrationStreamGate;
51
52
  const index_1 = require("./providers/index");
53
+ const approval_1 = require("./approval");
52
54
  const status_parser_1 = require("./orchestration/status-parser");
53
55
  const validation_1 = require("./orchestration/validation");
54
56
  const front_door_policy_1 = require("./orchestration/front-door-policy");
55
57
  const deterministic_path_1 = require("./orchestration/deterministic-path");
56
58
  const orchestrator_operating_prompt_1 = require("./orchestration/orchestrator-operating-prompt");
57
59
  const policy_prompt_1 = require("./orchestration/policy-prompt");
60
+ const safeguards_1 = require("./orchestration/safeguards");
58
61
  const orchestrator_blocked_prompt_1 = require("./orchestration/orchestrator-blocked-prompt");
59
62
  const orchestrator_final_response_prompt_1 = require("./orchestration/orchestrator-final-response-prompt");
60
63
  const worker_operating_prompt_1 = require("./orchestration/worker-operating-prompt");
64
+ const capabilities_1 = require("./orchestration/capabilities");
61
65
  const policy_detection_1 = require("./policy-detection");
62
66
  const execution_contract_1 = require("./execution-contract");
63
67
  const state_1 = require("./orchestration/state");
64
68
  const orchestrator_profile_1 = require("./orchestrator-profile");
65
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");
66
73
  const data = __importStar(require("./local-data"));
67
74
  const cli_session_epoch_1 = require("./cli-session-epoch");
68
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");
69
78
  const fs = __importStar(require("fs"));
70
79
  const os = __importStar(require("os"));
71
80
  const path = __importStar(require("path"));
72
- const ORCHESTRATOR_ROLE_SETTING_KEY = 'orchestrator_role_assignments';
73
81
  const ORCHESTRATOR_WORKFLOW_SETTING_KEY = 'orchestrator_preferred_workflow';
74
82
  const ORCHESTRATOR_TEMPLATE_SETTING_KEY = 'orchestrator_default_workflow_template_id';
75
83
  const HEARTBEAT_MS = 30_000;
@@ -84,11 +92,22 @@ const ORCHESTRATION_NODE_RETRY_LIMITS = {
84
92
  finalize_response: 0,
85
93
  require_confirmation: 0,
86
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
+ }
87
100
  // ─── Orchestrator Agent ──────────────────────────────────────────
88
- function resolveLocalProviderCredentials(providerName, profile) {
101
+ function resolveLocalProviderCredentials(providerName, profile, explicitConnection) {
89
102
  if (index_1.CLI_PROVIDERS.has(providerName)) {
90
103
  return { apiKey: 'cli-auth' };
91
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
+ }
92
111
  const directConnection = profile?.provider_connection_id
93
112
  ? data.getProviderConnection(profile.provider_connection_id)
94
113
  : undefined;
@@ -126,12 +145,16 @@ function resolveLocalProviderCredentials(providerName, profile) {
126
145
  }
127
146
  function buildLocalDesktopOrchestratorRuntime(selectedBot) {
128
147
  if (data.isClerkOrchestratorEnabled()) {
129
- const providerName = (data.getSetting('clerk_provider') || '').trim();
130
- const model = (data.getSetting('clerk_model') || '').trim() || 'default';
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';
131
154
  if (!providerName) {
132
155
  throw new Error('Clerk is selected as orchestrator, but no clerk provider is configured.');
133
156
  }
134
- const credentials = resolveLocalProviderCredentials(providerName);
157
+ const credentials = resolveLocalProviderCredentials(providerName, null, clerkConnection);
135
158
  const llm = (0, index_1.createProvider)(providerName, {
136
159
  apiKey: credentials.apiKey,
137
160
  model,
@@ -143,15 +166,23 @@ function buildLocalDesktopOrchestratorRuntime(selectedBot) {
143
166
  llm,
144
167
  providerName,
145
168
  model,
146
- agentName: 'Clerk',
169
+ agentName: 'Orchestrator',
147
170
  botId: null,
148
- modelLabel: model ? `${model} | Clerk` : 'Clerk',
171
+ modelLabel: model || null,
149
172
  };
150
173
  }
151
- const profile = selectedBot || data.getOrchestratorBot() || data.getDefaultAgentProfile();
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();
152
180
  if (!profile) {
153
- throw new Error('No orchestrator or default bot is configured.');
181
+ throw new Error('No orchestrator bot is configured. Mark a bot with is_orchestrator=1 or enable Clerk orchestration.');
154
182
  }
183
+ return buildLocalDesktopRuntimeFromProfile(profile);
184
+ }
185
+ function buildLocalDesktopRuntimeFromProfile(profile) {
155
186
  const credentials = resolveLocalProviderCredentials(profile.provider, profile);
156
187
  const llm = (0, index_1.createProvider)(profile.provider, {
157
188
  apiKey: credentials.apiKey,
@@ -169,6 +200,135 @@ function buildLocalDesktopOrchestratorRuntime(selectedBot) {
169
200
  modelLabel: profile.model || profile.name,
170
201
  };
171
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
+ }
172
332
  class OrchestratorAgent {
173
333
  orchestratorRuntime;
174
334
  workflowEngine;
@@ -191,9 +351,9 @@ class OrchestratorAgent {
191
351
  },
192
352
  providerName: runtimeInfo?.provider || 'openai',
193
353
  model: runtimeInfo?.model || null,
194
- agentName: 'Clerk',
354
+ agentName: 'Orchestrator',
195
355
  botId: null,
196
- modelLabel: runtimeInfo?.model ? `${runtimeInfo.model} | Clerk` : 'Clerk',
356
+ modelLabel: runtimeInfo?.model || null,
197
357
  };
198
358
  }
199
359
  else {
@@ -228,6 +388,8 @@ class OrchestratorAgent {
228
388
  return await work(attempt);
229
389
  }
230
390
  catch (error) {
391
+ if (isAbortLikeError(error))
392
+ throw error;
231
393
  if (attempt >= maxRetries)
232
394
  throw error;
233
395
  attempt += 1;
@@ -254,11 +416,52 @@ class OrchestratorAgent {
254
416
  add('GPT');
255
417
  return Array.from(refs);
256
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
+ }
257
458
  describeAgentResponsibilities(agent) {
258
459
  const candidates = [
259
460
  agent.purpose_md,
260
461
  agent.identity_summary,
261
462
  agent.skills_md,
463
+ agent.orchestration_role_label,
464
+ agent.orchestration_role_class,
262
465
  agent.role_label,
263
466
  agent.role_class,
264
467
  ].map((value) => String(value || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
@@ -273,7 +476,7 @@ class OrchestratorAgent {
273
476
  return cleaned.length > 180 ? `${cleaned.slice(0, 177).trim()}...` : cleaned;
274
477
  }
275
478
  }
276
- const normalizedRole = String(agent.role_class || agent.role_label || '').trim().toLowerCase();
479
+ const normalizedRole = this.getOrchestrationRoleClass(agent);
277
480
  if (normalizedRole === 'code' || normalizedRole === 'coding') {
278
481
  return 'implementation, code changes, fixes, and app changes';
279
482
  }
@@ -299,7 +502,7 @@ class OrchestratorAgent {
299
502
  const orchestrationState = (0, state_1.createOrchestrationState)({
300
503
  conversationId: conversationId || null,
301
504
  projectId: conversationProjectId || null,
302
- orchestratorBotId: conversation?.agent_id || null,
505
+ orchestratorBotId: opts.orchestratorBotIdHint || null,
303
506
  userPromptRaw: prompt,
304
507
  });
305
508
  orchestrationState.requestedArtifactTargets = this.extractRequestedArtifactTargets(prompt);
@@ -318,24 +521,36 @@ class OrchestratorAgent {
318
521
  return this.applyPendingPolicy(conversationId, pendingPolicy);
319
522
  }
320
523
  const promptAssignments = this.parseRoleAssignments(prompt);
321
- const selectedOrchestrator = this.resolveSelectedOrchestratorBot(conversation);
322
- 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) {
323
538
  this.setLastResponseMeta({
324
539
  agentName: 'Orchestrator',
325
540
  botId: selectedOrchestrator.id,
326
541
  modelLabel: null,
327
542
  });
328
543
  }
329
- if (selectedOrchestrator) {
330
- orchestrationState.orchestratorBotId = selectedOrchestrator.id;
544
+ if (dispatchOrchestratorBot) {
545
+ orchestrationState.orchestratorBotId = dispatchOrchestratorBot.id;
331
546
  }
332
547
  const initialProject = conversationProjectId ? data.getProject(conversationProjectId) : undefined;
333
548
  const initialPolicy = data.getEffectiveOrchestrationPolicy(conversationProjectId || undefined);
334
- 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);
335
550
  const initialOverview = conversationProjectId ? data.getProjectOverview(conversationProjectId) : undefined;
336
551
  // If a specific workflow template was selected by the user, skip front-door classification
337
552
  // and go straight to workflow execution.
338
- if (opts.workflowTemplateId && selectedOrchestrator) {
553
+ if (opts.workflowTemplateId && hasConfiguredOrchestratorRuntime) {
339
554
  const template = data.getWorkflowTemplate(opts.workflowTemplateId);
340
555
  if (template) {
341
556
  orchestrationState.intent = 'workflow';
@@ -354,14 +569,21 @@ class OrchestratorAgent {
354
569
  const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
355
570
  if (validation.ok) {
356
571
  this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
357
- 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);
358
580
  }
359
581
  }
360
582
  }
361
- const namedWorkflowTemplate = selectedOrchestrator
583
+ const namedWorkflowTemplate = hasConfiguredOrchestratorRuntime
362
584
  ? this.resolveWorkflowTemplateByPrompt(conversationProjectId, prompt)
363
585
  : undefined;
364
- if (namedWorkflowTemplate && selectedOrchestrator) {
586
+ if (namedWorkflowTemplate && hasConfiguredOrchestratorRuntime) {
365
587
  orchestrationState.intent = 'workflow';
366
588
  this.recordOrchestrationAudit(orchestrationState, 'choose_path', 'path_selected', `User explicitly named workflow template: ${namedWorkflowTemplate.name}`, { workflowTemplateId: namedWorkflowTemplate.id });
367
589
  const workflowIntent = {
@@ -378,8 +600,34 @@ class OrchestratorAgent {
378
600
  const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
379
601
  if (validation.ok) {
380
602
  this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
381
- 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);
611
+ }
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;
382
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.';
383
631
  }
384
632
  if (selectedOrchestrator) {
385
633
  const frontDoorResult = await this.handleOrchestratorFrontDoor(prompt, conversationId, opts, selectedOrchestrator, conversationProjectId, initialProject, initialPolicy, promptAssignments, initialAssignments, initialOverview, orchestrationState);
@@ -414,13 +662,14 @@ class OrchestratorAgent {
414
662
  const effectiveProjectId = refreshedConversation?.project_id || resolvedProjectId;
415
663
  const effectiveProject = effectiveProjectId ? data.getProject(effectiveProjectId) : undefined;
416
664
  const effectivePolicy = data.getEffectiveOrchestrationPolicy(effectiveProjectId || conversationProjectId || undefined);
417
- const effectiveOrchestrator = refreshedConversation ? this.resolveSelectedOrchestratorBot(refreshedConversation) : selectedOrchestrator;
418
- 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);
419
667
  if (effectiveProjectId &&
420
668
  this.hasRoleAssignments(promptAssignments) &&
421
669
  !this.hasMode(intent, 'POLICY_UPDATE')) {
422
- this.persistRoleAssignments(effectiveProjectId, conversationId, refreshedConversation?.agent_id || 'orchestrator', promptAssignments);
423
- 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);
424
673
  }
425
674
  const projectOverview = effectiveProjectId ? data.getProjectOverview(effectiveProjectId) : undefined;
426
675
  if (this.isOrchestratorControlMessage(prompt)) {
@@ -564,22 +813,15 @@ class OrchestratorAgent {
564
813
  buildFrontDoorEffectivePolicy(policy, orchestratorBot, roleAssignments, taskType) {
565
814
  const normalize = (value) => String(value || '').trim().toLowerCase();
566
815
  const orchestratorName = normalize(orchestratorBot.name);
567
- const roleClass = normalize(orchestratorBot.role_class);
568
816
  const codingOwner = normalize(roleAssignments.coding);
569
817
  const qaOwner = normalize(roleAssignments.qa);
570
818
  const next = { ...policy };
571
819
  const orchestratorOwnsCoding = orchestratorName.length > 0
572
820
  && (orchestratorName === codingOwner
573
- || roleClass === 'code'
574
- || roleClass === 'coding'
575
- || roleClass === 'coder'
576
- || roleClass === 'builder'
577
- || roleClass === 'developer');
821
+ || this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer']));
578
822
  const orchestratorOwnsQa = orchestratorName.length > 0
579
823
  && (orchestratorName === qaOwner
580
- || roleClass === 'qa'
581
- || roleClass === 'review'
582
- || roleClass === 'reviewer');
824
+ || this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']));
583
825
  if (taskType === 'coding' && orchestratorOwnsCoding) {
584
826
  next.allowOrchestratorCode = true;
585
827
  }
@@ -601,67 +843,16 @@ class OrchestratorAgent {
601
843
  const mentionedAgents = this.countMentionedAgents(prompt);
602
844
  return mentionedAgents === 1 && /\b(ask|let|have|talk to|discuss with|check with)\b/i.test(prompt);
603
845
  }
604
- applyClassificationGuards(prompt, intent, routingMode) {
605
- const before = JSON.stringify({
606
- primaryMode: intent.primaryMode,
607
- targetAgent: intent.targetAgent || null,
608
- needsClarification: !!intent.needsClarification,
609
- clarificationQuestions: intent.clarificationQuestions || [],
610
- });
611
- const targetAgent = this.extractTargetAgentName(prompt) || intent.targetAgent;
612
- const needsConcreteArtifact = /\b(this idea|review this idea|code it|qa it|review it|build it|fix it)\b/i.test(prompt)
613
- && !this.hasConcreteArtifactDetails(prompt);
614
- const explicitWorkflowRoles = this.inferRequestedWorkflowRoles(prompt, {});
615
- const explicitWorkflowRequest = explicitWorkflowRoles.length >= 2 && this.hasConcreteArtifactDetails(prompt);
616
- if (targetAgent && !intent.targetAgent) {
617
- intent.targetAgent = targetAgent;
618
- }
619
- if (explicitWorkflowRequest) {
620
- intent.primaryMode = 'WORKFLOW_MODE';
621
- intent.userFacingMode = 'FULL_WORKFLOW';
622
- intent.targetScope = 'MULTI_WORKER';
623
- intent.isMultiStep = true;
624
- intent.intent = 'build';
625
- intent.targetAgent = undefined;
626
- intent.needsClarification = false;
627
- intent.clarificationQuestions = undefined;
628
- intent.secondaryModes = intent.secondaryModes.filter((mode) => mode !== 'PROXY_MODE');
629
- intent.executionOrder = ['WORKFLOW_MODE'];
630
- }
631
- if (intent.primaryMode === 'PROXY_MODE' && !intent.targetAgent) {
632
- intent.needsClarification = true;
633
- intent.clarificationQuestions = Array.from(new Set([
634
- ...(intent.clarificationQuestions || []),
635
- 'Which worker should handle this request?',
636
- ]));
637
- }
638
- if (needsConcreteArtifact &&
639
- (intent.primaryMode === 'WORKFLOW_MODE' || intent.primaryMode === 'PROXY_MODE')) {
640
- intent.needsClarification = true;
641
- intent.clarificationQuestions = Array.from(new Set([
642
- ...(intent.clarificationQuestions || []),
643
- 'What specific idea, artifact, or content should the team work on?',
644
- ]));
645
- }
646
- if (intent.primaryMode === 'WORKFLOW_MODE' && !intent.isMultiStep && intent.targetScope !== 'MULTI_WORKER') {
647
- intent.needsClarification = true;
648
- intent.clarificationQuestions = Array.from(new Set([
649
- ...(intent.clarificationQuestions || []),
650
- 'What multi-step workflow do you want me to coordinate?',
651
- ]));
652
- }
653
- if (intent.needsClarification && this.hasConcreteArtifactDetails(prompt)) {
654
- const remaining = (intent.clarificationQuestions || []).filter((question) => !/specific idea, artifact, or content/i.test(question));
655
- intent.clarificationQuestions = remaining.length > 0 ? remaining : undefined;
656
- intent.needsClarification = Boolean(intent.clarificationQuestions?.length);
657
- }
658
- const after = JSON.stringify({
659
- primaryMode: intent.primaryMode,
660
- targetAgent: intent.targetAgent || null,
661
- needsClarification: !!intent.needsClarification,
662
- clarificationQuestions: intent.clarificationQuestions || [],
663
- });
664
- 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;
665
856
  }
666
857
  formatOrchestratorControlResponse(prompt, assignments, overview) {
667
858
  const lines = [];
@@ -714,82 +905,33 @@ class OrchestratorAgent {
714
905
  // Aliases like "claude" → "Ben" cause false matches on every prompt.
715
906
  return [agent.name.trim()].filter(Boolean);
716
907
  }
717
- fallbackClassifyUserMessage(prompt, routingMode) {
718
- const normalized = prompt.toLowerCase();
719
- const targetAgent = this.extractTargetAgentName(prompt);
720
- const hasPolicyLanguage = this.isExplicitPolicyMessage(prompt);
721
- 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);
722
- 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);
723
- const isGreeting = /\b(hello|hi|hey|are you there|good morning|good afternoon|good evening)\b/i.test(prompt);
724
- const needsWorkflow = /\b(todo|workflow|handoff|qa\b|review after|send .* then|first .* then|team workflow|multiple llms)\b/i.test(normalized)
725
- || (/\bbrain\b/i.test(prompt) && /\bben\b/i.test(prompt))
726
- || (/\bben\b/i.test(prompt) && /\bjohn\b/i.test(prompt));
727
- const isProxy = Boolean(targetAgent) || (routingMode === 'proxy' && !hasPolicyLanguage && !isStatus);
728
- let primaryMode = 'DIRECT_CONVERSATION';
729
- const secondaryModes = [];
730
- let intent = 'simple';
731
- let isMultiStep = false;
732
- if (hasPolicyLanguage) {
733
- primaryMode = 'POLICY_UPDATE';
734
- intent = 'plan';
735
- secondaryModes.push('DIRECT_CONVERSATION');
736
- }
737
- else if (hasMemoryLanguage) {
738
- primaryMode = 'MEMORY_CAPTURE';
739
- }
740
- else if (isStatus) {
741
- primaryMode = 'STATUS_INQUIRY';
742
- intent = 'question';
743
- }
744
- else if (needsWorkflow) {
745
- primaryMode = 'WORKFLOW_MODE';
746
- intent = 'build';
747
- isMultiStep = true;
748
- }
749
- else if (isProxy) {
750
- primaryMode = 'PROXY_MODE';
751
- intent = 'discuss';
752
- }
753
- else if (isGreeting || this.isProjectKnowledgeRequest(prompt) || this.isOrchestratorControlMessage(prompt)) {
754
- primaryMode = 'DIRECT_CONVERSATION';
755
- intent = 'question';
756
- }
757
- const executionOrder = [primaryMode];
758
- if (hasPolicyLanguage && needsWorkflow && !secondaryModes.includes('WORKFLOW_MODE')) {
759
- secondaryModes.push('WORKFLOW_MODE');
760
- executionOrder.push('WORKFLOW_MODE');
761
- }
908
+ /**
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.
918
+ */
919
+ fallbackClassifyUserMessage(_prompt, _routingMode) {
762
920
  return {
763
- primaryMode,
764
- secondaryModes,
765
- executionOrder,
766
- userFacingMode: primaryMode === 'POLICY_UPDATE'
767
- ? 'POLICY_CONFIGURATION'
768
- : primaryMode === 'STATUS_INQUIRY'
769
- ? 'STATUS_CHECK'
770
- : primaryMode === 'PROXY_MODE'
771
- ? 'ASK_WORKER'
772
- : primaryMode === 'WORKFLOW_MODE'
773
- ? 'FULL_WORKFLOW'
774
- : intent === 'plan'
775
- ? 'PLANNING'
776
- : 'DIRECT_RESPONSE',
777
- targetScope: primaryMode === 'PROXY_MODE'
778
- ? 'ONE_WORKER'
779
- : primaryMode === 'WORKFLOW_MODE'
780
- ? 'MULTI_WORKER'
781
- : 'SELF',
782
- confidence: hasPolicyLanguage || isStatus || needsWorkflow || isProxy ? 'MEDIUM' : 'LOW',
783
- intent,
921
+ primaryMode: 'DIRECT_CONVERSATION',
922
+ secondaryModes: [],
923
+ executionOrder: ['DIRECT_CONVERSATION'],
924
+ userFacingMode: 'DIRECT_RESPONSE',
925
+ targetScope: 'SELF',
926
+ confidence: 'LOW',
927
+ intent: 'simple',
784
928
  projectMatch: undefined,
785
929
  topicMatch: undefined,
786
- targetAgent,
787
- isMultiStep,
788
- reasoning: 'Fallback intent classification',
789
- needsClarification: primaryMode === 'PROXY_MODE' && this.isVagueProxyPrompt(prompt),
790
- clarificationQuestions: primaryMode === 'PROXY_MODE' && this.isVagueProxyPrompt(prompt)
791
- ? [`What specific idea, artifact, or content should ${targetAgent || 'the specialist'} review?`]
792
- : 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,
793
935
  };
794
936
  }
795
937
  isExplicitPolicyMessage(prompt) {
@@ -895,6 +1037,7 @@ class OrchestratorAgent {
895
1037
  this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Routing readiness ping to ${agent.name}`);
896
1038
  const result = await this.workflowEngine.execute(readinessPrompt, conversationId, agent.id, {
897
1039
  apiKey: this.resolveApiKey(agent.provider),
1040
+ abortSignal: opts.abortSignal,
898
1041
  disableDecomposition: true,
899
1042
  });
900
1043
  if (result.status === 'completed' || result.status === 'partial') {
@@ -1108,14 +1251,686 @@ class OrchestratorAgent {
1108
1251
  .replace(/\n?STATUS:\s*(PASS|FAIL|COMPLETE|BLOCKED|UNKNOWN)\s*$/i, '')
1109
1252
  .trim() || rawText.trim();
1110
1253
  }
1111
- resolveSelectedOrchestratorBot(conversation) {
1112
- const conversationBotId = conversation?.agent_id;
1113
- if (conversationBotId) {
1114
- 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);
1115
1272
  if (bot)
1116
1273
  return bot;
1117
1274
  }
1118
- 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.');
1119
1934
  }
1120
1935
  async handleOrchestratorFrontDoor(prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state) {
1121
1936
  (0, state_1.addHelperRoleUsage)(state, 'orchestrator_front_door');
@@ -1133,7 +1948,7 @@ class OrchestratorAgent {
1133
1948
  const PURE_ORCHESTRATOR_ROLES = new Set(['orchestrator', 'project manager']);
1134
1949
  const specialists = allProfiles
1135
1950
  .filter((agent) => {
1136
- const rc = String(agent.role_class || '').trim().toLowerCase();
1951
+ const rc = this.getOrchestrationRoleClass(agent);
1137
1952
  if (agent.is_orchestrator === 1 && PURE_ORCHESTRATOR_ROLES.has(rc))
1138
1953
  return false;
1139
1954
  if (!agent.is_orchestrator && (0, orchestrator_profile_1.isOrchestratorProfile)(agent))
@@ -1142,14 +1957,14 @@ class OrchestratorAgent {
1142
1957
  })
1143
1958
  .map((agent) => ({
1144
1959
  name: agent.name,
1145
- roleLabel: agent.role_label || agent.role_class || 'general',
1960
+ roleLabel: this.getOrchestrationRoleLabel(agent),
1146
1961
  references: this.getAgentRoutingReferences(agent),
1147
1962
  responsibilities: this.describeAgentResponsibilities(agent),
1148
1963
  }));
1149
1964
  const signalSpecialists = allProfiles
1150
1965
  .map((agent) => ({
1151
1966
  name: agent.name,
1152
- roleLabel: agent.role_label || agent.role_class || 'general',
1967
+ roleLabel: this.getOrchestrationRoleLabel(agent),
1153
1968
  references: this.getAgentRoutingReferences(agent),
1154
1969
  responsibilities: this.describeAgentResponsibilities(agent),
1155
1970
  }));
@@ -1181,16 +1996,52 @@ class OrchestratorAgent {
1181
1996
  });
1182
1997
  }
1183
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;
1184
2010
  const localPromptContract = useSinglePassOrchestrator
1185
2011
  ? (index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
1186
2012
  && conversationId
1187
- && !!(0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, orchestratorBot.id, orchestratorBot.provider).resumeSessionId
2013
+ && !!orchestratorCliSessionPlan?.resumeSessionId
1188
2014
  ? 'cli_recurring'
1189
2015
  : 'api_or_fresh_cli')
1190
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;
1191
2042
  const operatingPrompt = (0, orchestrator_operating_prompt_1.buildOrchestratorOperatingPrompt)({
1192
- orchestratorName: orchestratorBot.name,
1193
- primaryRole: orchestratorBot.role_label || orchestratorBot.role_class || null,
2043
+ orchestratorName: orchestratorDisplayName,
2044
+ primaryRole: isClerkRuntime ? 'project manager' : data.getAgentOrchestrationRoleLabel(orchestratorBot),
1194
2045
  specialists,
1195
2046
  workflowNames,
1196
2047
  localPromptContract,
@@ -1212,7 +2063,9 @@ class OrchestratorAgent {
1212
2063
  }),
1213
2064
  recentSummary: recentSummaryText,
1214
2065
  lastFiveTurns: recentTurnsText,
2066
+ bootstrapHistoryFilePath: orchestratorBootstrapHistoryFilePath,
1215
2067
  decisionOnly: !useSinglePassOrchestrator,
2068
+ defaultBotName: data.getDefaultAgentProfile()?.name || roleAssignments.coding || null,
1216
2069
  });
1217
2070
  const decisionPrompt = [
1218
2071
  'This is a routing decision only. Do not perform the task.',
@@ -1223,19 +2076,21 @@ class OrchestratorAgent {
1223
2076
  'Return strict JSON only.',
1224
2077
  ].join('\n');
1225
2078
  let decision = null;
1226
- this.reportHiddenRoleProgress(opts, 'orchestrator', 'Understanding request');
1227
- let understandingHeartbeat = setInterval(() => {
1228
- this.reportHiddenRoleProgress(opts, 'orchestrator', 'Still understanding the request');
1229
- }, 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);
1230
2083
  try {
1231
2084
  if (useSinglePassOrchestrator) {
1232
2085
  const result = await this.runNodeWithRetry('understand_request', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
2086
+ abortSignal: opts.abortSignal,
1233
2087
  disableDecomposition: true,
1234
2088
  isOrchestrated: false,
1235
2089
  projectId: projectId || null,
1236
2090
  workspacePath: project?.folder?.trim() || undefined,
1237
2091
  persistConversationMessages: false,
1238
2092
  workerMode: false,
2093
+ storedAttachments: opts.storedAttachments,
1239
2094
  systemPromptOverride: operatingPrompt,
1240
2095
  onWorkerChunk: opts.onWorkerChunk,
1241
2096
  onProgress: (progress) => {
@@ -1263,6 +2118,8 @@ class OrchestratorAgent {
1263
2118
  projectId: projectId || null,
1264
2119
  workspacePath: project?.folder?.trim() || undefined,
1265
2120
  disableTools: true,
2121
+ abortSignal: opts.abortSignal,
2122
+ onNarrationChunk: this.makeOrchestratorNarrationForwarder(opts, orchestratorBot),
1266
2123
  }));
1267
2124
  decision = (0, orchestrator_operating_prompt_1.parseOrchestratorFrontDoorDecision)(rawDecision);
1268
2125
  }
@@ -1322,7 +2179,20 @@ class OrchestratorAgent {
1322
2179
  };
1323
2180
  }
1324
2181
  }
1325
- 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);
1326
2196
  }
1327
2197
  buildFrontDoorSystemSuggestion(prompt, frontDoorSignals, promptAssignments, roleAssignments) {
1328
2198
  const normalizeName = (value) => String(value || '').trim().toLowerCase();
@@ -1367,7 +2237,7 @@ class OrchestratorAgent {
1367
2237
  delegateRole,
1368
2238
  };
1369
2239
  }
1370
- 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) {
1371
2241
  if (!decision) {
1372
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' });
1373
2243
  return null;
@@ -1375,21 +2245,16 @@ class OrchestratorAgent {
1375
2245
  const highRiskPrompt = this.isHighRiskPrompt(prompt);
1376
2246
  state.riskLevel = highRiskPrompt ? 'high' : state.riskLevel;
1377
2247
  this.recordOrchestrationAudit(state, 'risk_gate', 'risk_assessed', highRiskPrompt ? 'Prompt matched high-risk action patterns.' : 'No high-risk action patterns matched before execution.', { highRiskPrompt });
1378
- const normalizedDecision = (0, front_door_policy_1.applyFrontDoorPolicy)({
1379
- prompt,
1380
- initialDecision: decision,
1381
- orchestratorName: orchestratorBot.name,
1382
- orchestratorRoleClass: orchestratorBot.role_class,
1383
- promptAssignments,
1384
- roleAssignments,
1385
- specialists: signalSpecialists,
1386
- workflowNames,
1387
- highRisk: highRiskPrompt,
1388
- });
1389
- if (normalizedDecision.corrected) {
1390
- (0, state_1.markMisrouteCorrected)(state);
1391
- }
1392
- 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
+ };
1393
2258
  state.intent = decision.mode;
1394
2259
  state.taskType = normalizedDecision.taskType;
1395
2260
  this.recordOrchestrationAudit(state, 'choose_path', 'path_selected', normalizedDecision.correctionReason || decision.reason || `Orchestrator selected ${decision.mode}.`, {
@@ -1432,42 +2297,39 @@ class OrchestratorAgent {
1432
2297
  return response;
1433
2298
  }
1434
2299
  if (decision.mode === 'execute_self') {
1435
- if (this.orchestratorRuntime.kind === 'clerk') {
1436
- const fallbackDelegateTarget = roleAssignments.coding
1437
- || data.getDefaultAgentProfile()?.name
1438
- || data.listAgentProfiles().find((agent) => !(0, orchestrator_profile_1.isOrchestratorProfile)(agent))?.name;
1439
- if (fallbackDelegateTarget) {
1440
- const delegateIntent = {
1441
- primaryMode: 'PROXY_MODE',
1442
- secondaryModes: [],
1443
- executionOrder: ['PROXY_MODE'],
1444
- userFacingMode: 'ASK_WORKER',
1445
- targetScope: 'ONE_WORKER',
1446
- confidence: 'MEDIUM',
1447
- intent: normalizedDecision.taskType === 'qa'
1448
- ? 'review'
1449
- : normalizedDecision.taskType === 'research'
1450
- ? 'brainstorm'
1451
- : 'build',
1452
- targetAgent: fallbackDelegateTarget,
1453
- isMultiStep: false,
1454
- reasoning: 'Clerk orchestrator cannot own implementation work directly, so the task falls back to the default worker.',
1455
- };
1456
- const delegatePrompt = prompt;
1457
- const executionSpec = this.buildFrontDoorDelegateExecutionSpec(delegatePrompt, delegateIntent, fallbackDelegateTarget);
1458
- const guardrailQuestions = this.buildDeterministicGuardrailQuestions(executionSpec, roleAssignments, delegateIntent);
1459
- if (guardrailQuestions.length > 0) {
1460
- return this.formatClarificationResponse({ ...delegateIntent, clarificationQuestions: guardrailQuestions }, overview);
1461
- }
1462
- this.applyExecutionSpecToState(state, executionSpec);
1463
- return this.handleProxyRequest(delegatePrompt, conversationId, delegateIntent, opts, executionSpec, roleAssignments, state);
1464
- }
1465
- }
1466
- // No guardrails, no confirmation checkpoints, no validation blocking.
1467
- // Ben just does the work — like Claude CLI.
1468
2300
  (0, state_1.setOrchestrationPath)(state, 'direct', 'direct');
1469
2301
  const directResult = await this.executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state);
1470
- 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);
1471
2333
  }
1472
2334
  if (decision.mode === 'delegate') {
1473
2335
  const delegateTarget = this.resolveDelegatedAgentName(decision, roleAssignments);
@@ -1509,7 +2371,7 @@ class OrchestratorAgent {
1509
2371
  if (executionSpec.requiresConfirmation) {
1510
2372
  return this.createConfirmationCheckpoint(delegatePrompt, conversationId, delegateIntent, executionSpec, projectId, roleAssignments, state);
1511
2373
  }
1512
- 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);
1513
2375
  }
1514
2376
  if (decision.mode === 'workflow') {
1515
2377
  const workflowObjective = decision.workflow_request?.trim() || prompt;
@@ -1539,19 +2401,32 @@ class OrchestratorAgent {
1539
2401
  return this.createConfirmationCheckpoint(prompt, conversationId, workflowIntent, validation.executionSpec, projectId, roleAssignments, state);
1540
2402
  }
1541
2403
  this.recordOrchestrationAudit(state, 'run_workflow', 'delegated', 'Workflow execution is using the original user prompt as the execution source of truth.', { workflowObjective });
1542
- 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));
1543
2405
  }
1544
2406
  return null;
1545
2407
  }
1546
2408
  resolveDelegatedAgentName(decision, roleAssignments) {
1547
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
+ }
1548
2423
  if (target && target.toLowerCase() !== 'none')
1549
2424
  return target;
1550
- if (decision.delegate_role === 'coding')
2425
+ if (role === 'coding')
1551
2426
  return roleAssignments.coding;
1552
- if (decision.delegate_role === 'qa')
2427
+ if (role === 'qa')
1553
2428
  return roleAssignments.qa;
1554
- if (decision.delegate_role === 'research')
2429
+ if (role === 'research')
1555
2430
  return roleAssignments.research;
1556
2431
  return undefined;
1557
2432
  }
@@ -1615,7 +2490,7 @@ class OrchestratorAgent {
1615
2490
  locked: true,
1616
2491
  };
1617
2492
  }
1618
- async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state, orchestratorBot, delegateTarget) {
2493
+ async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state, orchestratorActor, delegateTarget) {
1619
2494
  const projectId = opts.projectId || state.projectId || null;
1620
2495
  const project = projectId ? data.getProject(projectId) : undefined;
1621
2496
  const targetAgent = this.findAgentByName(delegateTarget, project?.id || undefined);
@@ -1626,7 +2501,7 @@ class OrchestratorAgent {
1626
2501
  clarificationQuestions: [`I could not resolve the target bot "${delegateTarget}". Which worker should own this task?`],
1627
2502
  }, projectId ? data.getProjectOverview(projectId) : undefined);
1628
2503
  }
1629
- 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));
1630
2505
  const task = data.addTodoTask({
1631
2506
  projectId,
1632
2507
  conversationId,
@@ -1639,7 +2514,7 @@ class OrchestratorAgent {
1639
2514
  taskType: step.taskType,
1640
2515
  profileName: 'Programming',
1641
2516
  taskPrompt: step.taskPrompt,
1642
- actor: { actorType: 'orchestrator', actorId: orchestratorBot.id },
2517
+ actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
1643
2518
  });
1644
2519
  this.markConversationOrchestrated(conversationId);
1645
2520
  this.recordOrchestrationAudit(state, 'delegate_specialist', 'delegated', `Queued delegated TODO for ${targetAgent.name}.`, {
@@ -1659,14 +2534,14 @@ class OrchestratorAgent {
1659
2534
  ].join('\n');
1660
2535
  return state.finalResponseDraft;
1661
2536
  }
1662
- 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 });
1663
2538
  }
1664
- 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) {
1665
2540
  const projectId = project?.id || opts.projectId || state.projectId || null;
1666
2541
  const effectivePolicy = data.getEffectiveOrchestrationPolicy(projectId || undefined);
1667
2542
  const plannedSteps = workflowTemplate
1668
- ? this.buildTodoStepsFromTemplate(prompt, workflowTemplate, orchestratorBot, project?.name || null, project?.folder || null, effectivePolicy)
1669
- : 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);
1670
2545
  if (plannedSteps.length === 0) {
1671
2546
  const overview = projectId ? data.getProjectOverview(projectId) : undefined;
1672
2547
  return this.formatClarificationResponse({
@@ -1692,9 +2567,9 @@ class OrchestratorAgent {
1692
2567
  profileName: 'Programming',
1693
2568
  nextWorkerBotId: next?.owner.id || null,
1694
2569
  nextWorkerName: next?.owner.name || null,
1695
- nextWorkerRole: next ? (next.owner.role_label || next.owner.role_class || null) : null,
2570
+ nextWorkerRole: next ? data.getAgentOrchestrationRoleLabel(next.owner) : null,
1696
2571
  taskPrompt: step.taskPrompt,
1697
- actor: { actorType: 'orchestrator', actorId: orchestratorBot.id },
2572
+ actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
1698
2573
  });
1699
2574
  createdTasks.push(task);
1700
2575
  (0, state_1.addDelegateTarget)(state, step.owner.name);
@@ -1723,18 +2598,72 @@ class OrchestratorAgent {
1723
2598
  state.finalResponseDraft = lines.join('\n');
1724
2599
  return state.finalResponseDraft;
1725
2600
  }
1726
- return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, {
2601
+ return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, {
1727
2602
  taskIds: createdTasks.map((task) => task.id),
1728
2603
  workflowName: workflowTemplate?.name || null,
1729
2604
  });
1730
2605
  }
1731
- async dispatchQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, context) {
1732
- const maxSteps = 20;
1733
- for (let iteration = 0; iteration < maxSteps; iteration += 1) {
1734
- 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
+ }
1735
2657
  if (!task) {
1736
- return await this.finalizeQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, context.workflowName);
2658
+ return await this.finalizeQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, context.workflowName, completedTaskIds);
1737
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));
1738
2667
  const owner = task.owner_bot_id
1739
2668
  ? data.getAgentProfile(task.owner_bot_id)
1740
2669
  : this.findAgentByName(task.owner_name || undefined, project?.id || undefined);
@@ -1745,8 +2674,12 @@ class OrchestratorAgent {
1745
2674
  return message;
1746
2675
  }
1747
2676
  const taskPrompt = task.task_prompt?.trim() || task.details || task.title;
2677
+ if (opts.importedPlanRunId) {
2678
+ data.touchImportedPlanRun(opts.importedPlanRunId);
2679
+ }
1748
2680
  const result = await this.runNodeWithRetry('run_workflow', state, async () => this.workflowEngine.execute(taskPrompt, conversationId, owner.id, {
1749
2681
  apiKey: this.resolveApiKey(owner.provider),
2682
+ abortSignal: opts.abortSignal,
1750
2683
  taskId: task.id,
1751
2684
  stepDescription: task.title,
1752
2685
  disableDecomposition: true,
@@ -1754,6 +2687,10 @@ class OrchestratorAgent {
1754
2687
  projectId: project?.id || opts.projectId || state.projectId || null,
1755
2688
  workspacePath: project?.folder?.trim() || undefined,
1756
2689
  persistConversationMessages: false,
2690
+ storedAttachments: opts.storedAttachments,
2691
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
2692
+ importedPlanRunId: opts.importedPlanRunId,
2693
+ cliSessionScopeKey: opts.cliSessionScopeKey,
1757
2694
  onWorkerChunk: opts.onWorkerChunk,
1758
2695
  onProgress: (progress) => {
1759
2696
  if (progress.event === 'step-started') {
@@ -1767,11 +2704,19 @@ class OrchestratorAgent {
1767
2704
  }
1768
2705
  },
1769
2706
  }));
2707
+ if (opts.abortSignal?.aborted || isAbortLikeError(result?.error)) {
2708
+ state.finalResponseDraft = '';
2709
+ return '';
2710
+ }
1770
2711
  const stillActive = data.getTodoTask(task.id, 'active');
1771
2712
  const completed = data.getTodoTask(task.id, 'completed');
2713
+ if (opts.abortSignal?.aborted) {
2714
+ state.finalResponseDraft = '';
2715
+ return '';
2716
+ }
1772
2717
  if (stillActive?.blocker_summary && stillActive.blocker_question) {
1773
2718
  this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', `${owner.name} blocked on TODO: ${task.title}`, { taskId: task.id, blockerSummary: stillActive.blocker_summary });
1774
- return await this.handleBlockedTodoTask(stillActive, opts, state, orchestratorBot, project);
2719
+ return await this.handleBlockedTodoTask(stillActive, opts, state, orchestratorActor, project);
1775
2720
  }
1776
2721
  if (!completed) {
1777
2722
  const error = result.steps.find((step) => step.error)?.error || this.stripWorkerProtocol(result.mergedResult || '') || 'Worker did not complete the queued task.';
@@ -1780,19 +2725,46 @@ class OrchestratorAgent {
1780
2725
  this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { taskId: task.id });
1781
2726
  return message;
1782
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
+ }
1783
2753
  }
1784
- const message = 'The workflow exceeded the automatic dispatch limit before completing.';
1785
- state.finalResponseDraft = message;
1786
- this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxSteps: 20 });
1787
- return message;
1788
2754
  }
1789
- async handleBlockedTodoTask(task, opts, state, orchestratorBot, project) {
2755
+ async handleBlockedTodoTask(task, opts, state, orchestratorActor, project) {
1790
2756
  const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
1791
2757
  const artifacts = data.listTodoArtifactsForTask(task.id, 'active').map((item) => item.path_or_ref);
1792
2758
  const completedTasks = data.listCompletedTodoTasksForConversation(task.conversation_id || state.conversationId || '');
1793
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
+ }
1794
2766
  const blockedPrompt = (0, orchestrator_blocked_prompt_1.buildBlockedWorkerOrchestratorPrompt)({
1795
- orchestratorName: orchestratorBot.name,
2767
+ orchestratorName: orchestratorActor.name,
1796
2768
  workerName: task.owner_name || 'Worker',
1797
2769
  workerRole,
1798
2770
  taskTitle: task.title,
@@ -1811,10 +2783,11 @@ class OrchestratorAgent {
1811
2783
  try {
1812
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.`, {
1813
2785
  conversationId: task.conversation_id || state.conversationId || null,
1814
- orchestratorBot,
2786
+ orchestratorBot: orchestratorActor.bot || undefined,
1815
2787
  projectId: project?.id || opts.projectId || state.projectId || null,
1816
2788
  workspacePath: project?.folder?.trim() || undefined,
1817
2789
  disableTools: true,
2790
+ abortSignal: opts.abortSignal,
1818
2791
  }));
1819
2792
  }
1820
2793
  catch (error) {
@@ -1827,8 +2800,14 @@ class OrchestratorAgent {
1827
2800
  state.finalResponseDraft = response;
1828
2801
  return response;
1829
2802
  }
1830
- async finalizeQueuedTodoChain(conversationId, opts, state, orchestratorBot, project, workflowName) {
1831
- 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) => ({
1832
2811
  ...task,
1833
2812
  artifactRefs: data.listTodoArtifactsForTask(task.id, 'completed').map((item) => item.path_or_ref),
1834
2813
  }));
@@ -1838,7 +2817,7 @@ class OrchestratorAgent {
1838
2817
  }
1839
2818
  const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
1840
2819
  const finalPrompt = (0, orchestrator_final_response_prompt_1.buildOrchestratorFinalResponsePrompt)({
1841
- orchestratorName: orchestratorBot.name,
2820
+ orchestratorName: orchestratorActor.name,
1842
2821
  projectName: project?.name || null,
1843
2822
  workspacePath: project?.folder || null,
1844
2823
  effectivePolicy: policy,
@@ -1848,10 +2827,11 @@ class OrchestratorAgent {
1848
2827
  try {
1849
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.`, {
1850
2829
  conversationId,
1851
- orchestratorBot,
2830
+ orchestratorBot: orchestratorActor.bot || undefined,
1852
2831
  projectId: project?.id || opts.projectId || state.projectId || null,
1853
2832
  workspacePath: project?.folder?.trim() || undefined,
1854
2833
  disableTools: true,
2834
+ abortSignal: opts.abortSignal,
1855
2835
  }));
1856
2836
  }
1857
2837
  catch (error) {
@@ -1867,27 +2847,31 @@ class OrchestratorAgent {
1867
2847
  : 'Completed automatic TODO dispatch.', { completedTaskCount: completedTasks.length });
1868
2848
  return response;
1869
2849
  }
1870
- buildSingleDelegateTodoStep(prompt, owner, orchestratorBot, projectName, workspacePath, effectivePolicy) {
2850
+ buildSingleDelegateTodoStep(prompt, conversationId, owner, orchestratorActor, projectName, workspacePath, effectivePolicy) {
1871
2851
  const taskType = this.normalizeTaskTypeForWorker(owner, 'coding');
2852
+ const recentContext = this.buildRecentConversationContextForWorker(conversationId);
1872
2853
  return {
1873
- title: `${owner.name}: ${this.summarizeTodoTitle(prompt)}`,
1874
- 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.`,
1875
2856
  owner,
1876
2857
  taskType,
1877
2858
  taskPrompt: this.buildWorkerTaskPrompt({
1878
2859
  originalPrompt: prompt,
1879
- stepInstruction: 'Handle this delegated request directly.',
2860
+ stepInstruction: 'Handle the user request from this conversation.',
1880
2861
  owner,
1881
2862
  previousWorker: null,
1882
2863
  nextWorker: null,
2864
+ conversationId,
1883
2865
  projectName,
1884
2866
  workspacePath,
1885
2867
  effectivePolicy,
2868
+ recentContext,
1886
2869
  }),
1887
2870
  successCriteria: `Complete the delegated ${taskType} task and return a concise result summary.`,
1888
2871
  };
1889
2872
  }
1890
- buildTodoStepsFromTemplate(prompt, workflowTemplate, orchestratorBot, projectName, workspacePath, effectivePolicy) {
2873
+ buildTodoStepsFromTemplate(prompt, conversationId, workflowTemplate, orchestratorActor, projectName, workspacePath, effectivePolicy) {
2874
+ const recentContext = this.buildRecentConversationContextForWorker(conversationId);
1891
2875
  const steps = workflowTemplate.steps
1892
2876
  .sort((a, b) => a.order_index - b.order_index)
1893
2877
  .map((step) => {
@@ -1913,14 +2897,315 @@ class OrchestratorAgent {
1913
2897
  owner: step.owner,
1914
2898
  previousWorker: steps[index - 1]?.owner || null,
1915
2899
  nextWorker: steps[index + 1]?.owner || null,
2900
+ conversationId,
1916
2901
  projectName,
1917
2902
  workspacePath,
1918
2903
  effectivePolicy,
2904
+ recentContext,
1919
2905
  }),
1920
2906
  successCriteria: `Complete step ${index + 1} of "${workflowTemplate.name}" and hand off the necessary output.`,
1921
2907
  }));
1922
2908
  }
1923
- 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);
1924
3209
  const roleOrder = this.inferWorkflowRoleOrder(prompt, roleAssignments, intent);
1925
3210
  const owners = roleOrder
1926
3211
  .map((role) => {
@@ -1933,7 +3218,7 @@ class OrchestratorAgent {
1933
3218
  .filter((item) => Boolean(item));
1934
3219
  return owners.map((item, index) => ({
1935
3220
  title: `${item.agent.name}: ${this.defaultTodoTitleForRole(item.role, prompt)}`,
1936
- details: `Workflow step assigned by ${orchestratorBot.name}.`,
3221
+ details: `Workflow step assigned by ${orchestratorActor.name}.`,
1937
3222
  owner: item.agent,
1938
3223
  taskType: this.normalizeTaskTypeForWorker(item.agent, item.role === 'research' ? 'research' : item.role === 'qa' ? 'qa' : 'coding'),
1939
3224
  taskPrompt: this.buildWorkerTaskPrompt({
@@ -1942,80 +3227,75 @@ class OrchestratorAgent {
1942
3227
  owner: item.agent,
1943
3228
  previousWorker: owners[index - 1]?.agent || null,
1944
3229
  nextWorker: owners[index + 1]?.agent || null,
3230
+ conversationId,
1945
3231
  projectName: projectName || null,
1946
3232
  workspacePath: workspacePath || null,
1947
3233
  effectivePolicy: effectivePolicy || data.getEffectiveOrchestrationPolicy(projectId),
3234
+ recentContext,
1948
3235
  }),
1949
3236
  successCriteria: this.defaultSuccessCriteriaForRole(item.role),
1950
3237
  }));
1951
3238
  }
1952
- inferWorkflowRoleOrder(prompt, assignments, intent) {
1953
- const explicitResearch = /\b(research|brainstorm|plan|planning|analyze|investigate|design|architect)\b/i.test(prompt);
1954
- 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);
1955
- const explicitQa = /\b(qa|review|test|verify|verification|audit)\b/i.test(prompt);
1956
- const requestedRoles = [];
1957
- if (explicitResearch && assignments.research)
1958
- requestedRoles.push('research');
1959
- if (explicitCoding && assignments.coding)
1960
- requestedRoles.push('coding');
1961
- if (explicitQa && assignments.qa)
1962
- requestedRoles.push('qa');
1963
- if (requestedRoles.length > 0)
1964
- return requestedRoles;
1965
- if (intent.intent === 'review' && assignments.qa)
1966
- return ['qa'];
1967
- if (intent.intent === 'brainstorm' && assignments.research)
1968
- return ['research'];
1969
- const fallback = [];
1970
- if (assignments.research && /\b(research|brainstorm|plan|planning)\b/i.test(prompt))
1971
- fallback.push('research');
1972
- if (assignments.coding)
1973
- fallback.push('coding');
1974
- if (assignments.qa)
1975
- fallback.push('qa');
1976
- if (fallback.length > 0)
1977
- 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) {
1978
3257
  return [];
1979
3258
  }
1980
3259
  normalizeTaskTypeForWorker(owner, fallback) {
1981
- const roleClass = String(owner.role_class || owner.role_label || '').trim().toLowerCase();
1982
- 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']))
1983
3265
  return 'qa';
1984
- if (roleClass === 'research' || roleClass === 'brainstorm' || roleClass === 'planning' || roleClass === 'plan')
3266
+ if (this.agentHasAnyRole(owner, ['research', 'researcher', 'analyst', 'advisor', 'brainstorm', 'brainstorming', 'planning', 'plan', 'design']))
1985
3267
  return 'research';
1986
- if (roleClass === 'verify' || roleClass === 'verification')
3268
+ if (this.agentHasAnyRole(owner, ['verify', 'verification']))
1987
3269
  return 'verify';
1988
- if (roleClass === 'deploy' || roleClass === 'deployment')
1989
- return 'deploy';
1990
- if (roleClass === 'coding' || roleClass === 'code' || roleClass === 'builder' || roleClass === 'developer' || roleClass === 'engineer')
3270
+ if (this.agentHasAnyRole(owner, ['coding', 'code', 'builder', 'developer', 'engineer']))
1991
3271
  return 'coding';
3272
+ if (this.agentHasAnyRole(owner, ['deploy', 'deployment']))
3273
+ return 'deploy';
1992
3274
  return (fallback || 'coding');
1993
3275
  }
1994
3276
  buildWorkerTaskPrompt(input) {
1995
- const workerRole = String(input.owner.role_label || input.owner.role_class || 'general').trim() || 'general';
3277
+ const workerRole = this.getOrchestrationRoleLabel(input.owner);
1996
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);
1997
3282
  const composedTask = [
1998
3283
  `Step instruction: ${input.stepInstruction}`,
1999
3284
  taskType === 'qa'
2000
- ? '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.'
2001
3286
  : taskType === 'verify'
2002
3287
  ? 'Verification scope: Compare the delivered work against the original user request and the explicit success criteria.'
2003
3288
  : '',
2004
- String(input.nextWorker?.role_label || input.nextWorker?.role_class || '').trim().toLowerCase() === 'qa'
2005
- ? '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.'
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.'
2006
3291
  : '',
2007
- String(input.nextWorker?.role_label || input.nextWorker?.role_class || '').trim().toLowerCase() === 'qa'
2008
- ? '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.'
2009
- : '',
2010
- taskType === 'qa'
2011
- ? 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.'
2012
3294
  : '',
2013
- taskType === 'qa'
2014
- ? null
2015
- : 'Original user request (reference only):',
2016
- taskType === 'qa'
2017
- ? null
2018
- : 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,
2019
3299
  ].join('\n');
2020
3300
  return (0, worker_operating_prompt_1.buildWorkerOperatingPrompt)({
2021
3301
  workerName: input.owner.name,
@@ -2030,26 +3310,60 @@ class OrchestratorAgent {
2030
3310
  defaultLine: 'No confirmed special policy is set.',
2031
3311
  }),
2032
3312
  previousWorkerName: input.previousWorker?.name || null,
2033
- previousWorkerRole: input.previousWorker ? String(input.previousWorker.role_label || input.previousWorker.role_class || 'general') : null,
3313
+ previousWorkerRole: input.previousWorker ? this.getOrchestrationRoleLabel(input.previousWorker) : null,
2034
3314
  nextWorkerName: input.nextWorker?.name || null,
2035
- 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,
2036
3350
  });
2037
3351
  }
2038
- defaultTodoTitleForRole(role, prompt) {
3352
+ defaultTodoTitleForRole(role, _prompt) {
2039
3353
  if (role === 'research')
2040
- return `Research and plan: ${this.summarizeTodoTitle(prompt)}`;
3354
+ return 'Research and plan the user request';
2041
3355
  if (role === 'qa')
2042
- return `QA and review: ${this.summarizeTodoTitle(prompt)}`;
2043
- return `Build and implement: ${this.summarizeTodoTitle(prompt)}`;
3356
+ return 'QA and review the delivered work';
3357
+ return 'Build and implement the requested work';
2044
3358
  }
2045
- defaultStepInstructionForRole(role, prompt) {
3359
+ defaultStepInstructionForRole(role, _prompt) {
2046
3360
  if (role === 'research') {
2047
- 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.';
2048
3362
  }
2049
3363
  if (role === 'qa') {
2050
- 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.';
2051
3365
  }
2052
- 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.';
2053
3367
  }
2054
3368
  defaultSuccessCriteriaForRole(role) {
2055
3369
  if (role === 'research')
@@ -2083,10 +3397,65 @@ class OrchestratorAgent {
2083
3397
  return scopedMatch;
2084
3398
  return allAgents.find((agent) => agent.name.trim().toLowerCase() === normalized);
2085
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
+ }
2086
3416
  async executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state, options) {
2087
- this.recordOrchestrationAudit(state, 'execute_direct', 'executed', `Selected orchestrator bot ${orchestratorBot.name} kept the work.`, { orchestratorBotId: orchestratorBot.id, orchestratorBotName: orchestratorBot.name });
2088
- 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
+ }
2089
3457
  const result = await this.runNodeWithRetry('execute_direct', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
3458
+ abortSignal: opts.abortSignal,
2090
3459
  disableDecomposition: true,
2091
3460
  isOrchestrated: false,
2092
3461
  projectId: project?.id || state.projectId || null,
@@ -2096,6 +3465,7 @@ class OrchestratorAgent {
2096
3465
  // also persist would create duplicate assistant messages.
2097
3466
  persistConversationMessages: false,
2098
3467
  workerMode: false,
3468
+ storedAttachments: opts.storedAttachments,
2099
3469
  onWorkerChunk: opts.onWorkerChunk,
2100
3470
  onProgress: (progress) => {
2101
3471
  if (progress.event === 'step-failed') {
@@ -2165,12 +3535,15 @@ class OrchestratorAgent {
2165
3535
  (0, state_1.addHelperRoleUsage)(state, 'dispatch_controller');
2166
3536
  (0, state_1.addDelegateTarget)(state, delegateBot.name);
2167
3537
  const result = await this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(delegationPrompt, conversationId, delegateBot.id, {
3538
+ abortSignal: opts.abortSignal,
2168
3539
  disableDecomposition: true,
2169
3540
  isOrchestrated: false,
2170
3541
  workerMode: false,
2171
3542
  projectId: project?.id || state.projectId || null,
2172
3543
  workspacePath: project?.folder?.trim() || undefined,
2173
3544
  persistConversationMessages: true,
3545
+ storedAttachments: opts.storedAttachments,
3546
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
2174
3547
  onWorkerChunk: opts.onWorkerChunk,
2175
3548
  onProgress: (progress) => {
2176
3549
  if (progress.event === 'step-started') {
@@ -2195,9 +3568,25 @@ class OrchestratorAgent {
2195
3568
  return 'research';
2196
3569
  return 'discuss';
2197
3570
  }
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
+ }
2198
3578
  async runOrchestratorPrompt(userPrompt, systemPrompt, opts) {
2199
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;
2200
3588
  const result = await this.workflowEngine.execute(userPrompt, opts.conversationId, opts.orchestratorBot.id, {
3589
+ abortSignal: opts.abortSignal,
2201
3590
  disableDecomposition: true,
2202
3591
  disableTools: opts.disableTools === true,
2203
3592
  isOrchestrated: false,
@@ -2205,7 +3594,18 @@ class OrchestratorAgent {
2205
3594
  workspacePath: opts.workspacePath,
2206
3595
  persistConversationMessages: false,
2207
3596
  workerMode: false,
3597
+ storedAttachments: opts.storedAttachments,
2208
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
+ } : {}),
2209
3609
  });
2210
3610
  this.setLastResponseMeta({
2211
3611
  agentName: opts.orchestratorBot.name,
@@ -2214,60 +3614,57 @@ class OrchestratorAgent {
2214
3614
  });
2215
3615
  return this.stripWorkerProtocol(result.mergedResult || '');
2216
3616
  }
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;
2217
3621
  const response = await this.orchestratorRuntime.llm.chat({
2218
3622
  messages: [{ role: 'user', content: userPrompt }],
2219
3623
  system: systemPrompt,
2220
- stream: false,
3624
+ stream: !!onChunk,
2221
3625
  runtimeMode: 'local_desktop',
3626
+ abortSignal: opts.abortSignal,
3627
+ ...(onChunk ? { onChunk } : {}),
2222
3628
  });
2223
3629
  this.setLastResponseMeta({
2224
- agentName: this.orchestratorRuntime.agentName,
3630
+ agentName: this.isLocalDesktopRuntime() ? 'Orchestrator' : this.orchestratorRuntime.agentName,
2225
3631
  botId: this.orchestratorRuntime.botId,
2226
3632
  modelLabel: this.orchestratorRuntime.modelLabel,
2227
3633
  });
2228
3634
  return String(response.content || '').trim();
2229
3635
  }
2230
- inferRequestedWorkflowRoles(prompt, roleAssignments) {
2231
- const normalized = String(prompt || '').toLowerCase();
2232
- 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)
2233
- || /\b(best idea|best approach|not sure what to do)\b/.test(normalized);
2234
- 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);
2235
- const explicitCoding = /\b(ben|claude|code|build|implement|create|write|fix|edit|html|page|ui|ux|frontend|backend|file)\b/.test(normalized);
2236
- const onlyCodingAndQa = /\bonly\s+coding\s+and\s+qa\b/.test(normalized) || /\bno\s+research\b/.test(normalized);
2237
- 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);
2238
- const explicitCoderQaPair = explicitCoding && explicitQa;
2239
- const explicitResearchChain = explicitResearch && (explicitCoding || explicitQa);
2240
- if (!mentionsWorkflow && !explicitCoderQaPair && !explicitResearchChain) {
2241
- return [];
2242
- }
2243
- const roles = [];
2244
- if (explicitResearch && !onlyCodingAndQa)
2245
- roles.push('research');
2246
- if (explicitCoding || explicitCoderQaPair || explicitResearchChain)
2247
- roles.push('coding');
2248
- if (explicitQa || explicitCoderQaPair || explicitResearchChain)
2249
- roles.push('qa');
2250
- if (roles.length >= 2)
2251
- return roles;
2252
- if (mentionsWorkflow && roleAssignments.coding && roleAssignments.qa) {
2253
- return roleAssignments.research && !onlyCodingAndQa
2254
- ? ['research', 'coding', 'qa']
2255
- : ['coding', 'qa'];
2256
- }
2257
- return roles;
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 [];
2258
3656
  }
2259
3657
  orchestratorOwnsWorkflowRole(role, orchestratorBot, roleAssignments) {
2260
3658
  const orchestratorName = orchestratorBot.name.toLowerCase();
2261
3659
  const assignedName = roleAssignments[role]?.toLowerCase();
2262
3660
  if (assignedName)
2263
3661
  return assignedName === orchestratorName;
2264
- const roleClass = String(orchestratorBot.role_class || '').trim().toLowerCase();
2265
3662
  if (role === 'coding')
2266
- return ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager'].includes(roleClass);
3663
+ return this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager']);
2267
3664
  if (role === 'qa')
2268
- return ['qa', 'review', 'reviewer'].includes(roleClass);
3665
+ return this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']);
2269
3666
  if (role === 'research')
2270
- return ['research', 'researcher', 'analyst', 'advisor'].includes(roleClass);
3667
+ return this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design']);
2271
3668
  return false;
2272
3669
  }
2273
3670
  resolveWorkflowRoleTarget(role, orchestratorBot, roleAssignments) {
@@ -2363,7 +3760,7 @@ class OrchestratorAgent {
2363
3760
  'Do not build the final artifact in this step.',
2364
3761
  'Your deliverable is a concrete implementation direction document that the coding step can use immediately.',
2365
3762
  '',
2366
- `Original user request (reference only):\n${originalPrompt}`,
3763
+ `Original user request:\n${originalPrompt}`,
2367
3764
  artifactContext,
2368
3765
  '',
2369
3766
  'Your job: research the request, pressure-test the approach, and recommend the best implementation direction.',
@@ -2377,7 +3774,7 @@ class OrchestratorAgent {
2377
3774
  'Do not perform QA in this step.',
2378
3775
  'Your deliverable is the actual file or code change requested by the user.',
2379
3776
  '',
2380
- `Original user request (reference only):\n${originalPrompt}`,
3777
+ `Original user request:\n${originalPrompt}`,
2381
3778
  artifactContext,
2382
3779
  researchContext,
2383
3780
  qaContext,
@@ -2395,7 +3792,7 @@ class OrchestratorAgent {
2395
3792
  'You are the QA/review step in a multi-step workflow.',
2396
3793
  'Do not redo the coding or research work in this step unless the file is missing and you must report that blocker.',
2397
3794
  '',
2398
- `Original user request (reference only):\n${originalPrompt}`,
3795
+ `Original user request:\n${originalPrompt}`,
2399
3796
  artifactContext,
2400
3797
  researchContext,
2401
3798
  implementationContext,
@@ -2604,6 +4001,23 @@ class OrchestratorAgent {
2604
4001
  });
2605
4002
  }
2606
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
+ };
2607
4021
  // Update conversation routing mode to 'proxy'
2608
4022
  try {
2609
4023
  const db = data.getDb();
@@ -2613,14 +4027,17 @@ class OrchestratorAgent {
2613
4027
  // Execute single step — disable decomposition for proxy mode
2614
4028
  const runProxyStep = (stepPrompt) => this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(stepPrompt, conversationId, agent.id, {
2615
4029
  apiKey: this.resolveApiKey(agent.provider),
4030
+ abortSignal: opts.abortSignal,
2616
4031
  disableDecomposition: true,
2617
- isOrchestrated: true,
4032
+ isOrchestrated: false,
2618
4033
  projectId: state?.projectId ?? null,
2619
4034
  workspacePath: state?.projectId
2620
4035
  ? data.getProject(state.projectId)?.folder?.trim() || undefined
2621
4036
  : undefined,
2622
4037
  persistConversationMessages: false,
2623
- onWorkerChunk: opts.onWorkerChunk,
4038
+ storedAttachments: opts.storedAttachments,
4039
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
4040
+ onWorkerChunk: forwardProxyWorkerChunk,
2624
4041
  onProgress: (progress) => {
2625
4042
  if (progress.event === 'step-started') {
2626
4043
  this.reportHiddenRoleProgress(opts, 'orchestrator', `${progress.step.agentName} is working on the delegated step`);
@@ -2888,17 +4305,22 @@ class OrchestratorAgent {
2888
4305
  coderRetryLimit: effectivePolicy.coderRetryLimit,
2889
4306
  workspacePath: effectiveWorkspacePath,
2890
4307
  projectId: project?.id || opts.projectId || null,
4308
+ storedAttachments: opts.storedAttachments,
4309
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
2891
4310
  onProgress: progressHandler,
2892
4311
  onWorkerChunk: opts.onWorkerChunk,
2893
4312
  })
2894
4313
  : this.workflowEngine.execute(prompt, conversationId, defaultAgent.id, {
2895
4314
  apiKey: this.resolveApiKey(defaultAgent.provider),
4315
+ abortSignal: opts.abortSignal,
2896
4316
  taskId: todoTaskId,
2897
4317
  isOrchestrated: true,
2898
4318
  roleAssignments,
2899
4319
  coderRetryLimit: effectivePolicy.coderRetryLimit,
2900
4320
  workspacePath: effectiveWorkspacePath,
2901
4321
  projectId: project?.id || opts.projectId || null,
4322
+ storedAttachments: opts.storedAttachments,
4323
+ workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
2902
4324
  onProgress: progressHandler,
2903
4325
  onWorkerChunk: opts.onWorkerChunk,
2904
4326
  }));
@@ -3047,6 +4469,83 @@ class OrchestratorAgent {
3047
4469
  reportHiddenRoleProgress(opts, roleName, detail) {
3048
4470
  opts.onProgress(`${roleName}::${detail}`);
3049
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
+ }
3050
4549
  formatConfirmationRequest(prompt) {
3051
4550
  const singleLine = String(prompt || '').replace(/\s+/g, ' ').trim();
3052
4551
  if (!singleLine)
@@ -3168,58 +4667,126 @@ class OrchestratorAgent {
3168
4667
  hasRoleAssignments(assignments) {
3169
4668
  return Boolean(assignments.coding || assignments.qa || assignments.research);
3170
4669
  }
3171
- loadRoleAssignments(projectId) {
3172
- const raw = data.getProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY);
3173
- if (!raw)
3174
- return {};
3175
- try {
3176
- const parsed = JSON.parse(raw);
3177
- return {
3178
- coding: parsed.coding || undefined,
3179
- qa: parsed.qa || undefined,
3180
- research: parsed.research || undefined,
3181
- };
3182
- }
3183
- catch {
3184
- return {};
3185
- }
3186
- }
3187
- mergeRoleAssignments(base, saved, inferred, incoming) {
4670
+ mergeRoleAssignments(base, inferred, incoming) {
3188
4671
  return {
3189
- coding: incoming.coding || saved.coding || base.coding || inferred.coding,
3190
- qa: incoming.qa || saved.qa || base.qa || inferred.qa,
3191
- 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,
3192
4675
  };
3193
4676
  }
3194
4677
  deriveRoleAssignmentsFromProject(projectId, orchestratorBot) {
3195
4678
  const bots = this.listProjectScopedBots(projectId);
3196
4679
  const result = {};
3197
- const roleClass = String(orchestratorBot?.role_class || '').trim().toLowerCase();
3198
4680
  if (orchestratorBot?.name) {
3199
- if (['code', 'coding', 'coder', 'builder', 'developer', 'engineer'].includes(roleClass)) {
4681
+ if (this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])) {
3200
4682
  result.coding = orchestratorBot.name;
3201
4683
  }
3202
- else if (['qa', 'review', 'reviewer'].includes(roleClass)) {
4684
+ else if (this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer'])) {
3203
4685
  result.qa = orchestratorBot.name;
3204
4686
  }
3205
- else if (['research', 'researcher', 'analyst', 'advisor'].includes(roleClass)) {
4687
+ else if (this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])) {
3206
4688
  result.research = orchestratorBot.name;
3207
4689
  }
3208
4690
  }
3209
- 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));
3210
4692
  result.coding = result.coding || findByRole(['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])?.name;
3211
4693
  result.qa = result.qa || findByRole(['qa', 'review', 'reviewer'])?.name;
3212
- result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor'])?.name;
4694
+ result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])?.name;
3213
4695
  return result;
3214
4696
  }
3215
- listProjectScopedBots(projectId) {
4697
+ listProjectScopedBots(projectId, opts) {
4698
+ const maybeFilterOrchestrators = (bots) => opts?.includeOrchestratorProfiles ? bots : (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(bots);
3216
4699
  if (!projectId)
3217
- return (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
4700
+ return maybeFilterOrchestrators(data.listAgentProfiles());
3218
4701
  const project = data.getProject(projectId);
3219
4702
  const botIds = new Set(project?.bot_ids || []);
3220
4703
  const allBots = data.listAgentProfiles();
3221
4704
  const scoped = allBots.filter((bot) => botIds.has(bot.id));
3222
- 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();
3223
4790
  }
3224
4791
  policyRoleAssignments(policy) {
3225
4792
  return {
@@ -3424,35 +4991,6 @@ class OrchestratorAgent {
3424
4991
  }
3425
4992
  return undefined;
3426
4993
  }
3427
- persistRoleAssignments(projectId, conversationId, agentId, assignments) {
3428
- const merged = this.mergeRoleAssignments({}, this.loadRoleAssignments(projectId), this.deriveRoleAssignmentsFromProject(projectId), assignments);
3429
- data.setProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY, JSON.stringify(merged));
3430
- const parts = [
3431
- merged.coding ? `${merged.coding} handles coding` : null,
3432
- merged.qa ? `${merged.qa} handles QA` : null,
3433
- merged.research ? `${merged.research} handles research` : null,
3434
- ].filter(Boolean);
3435
- if (parts.length > 0 && (0, storage_mode_1.isServerStorageMode)()) {
3436
- data.upsertMemoryFact({
3437
- agentId,
3438
- conversationId,
3439
- factType: 'operating_instruction',
3440
- content: `Default orchestrator assignments: ${parts.join('; ')}.`,
3441
- extractionMethod: 'orchestrator',
3442
- });
3443
- }
3444
- data.logAdminAudit({
3445
- actorType: 'orchestrator',
3446
- actorId: 'Project Manager',
3447
- projectId,
3448
- resourceType: 'project_setting',
3449
- resourceId: `${projectId}:${ORCHESTRATOR_ROLE_SETTING_KEY}`,
3450
- action: 'set_orchestrator_role_assignments',
3451
- request: assignments,
3452
- after: merged,
3453
- result: 'ok',
3454
- });
3455
- }
3456
4994
  roleAssignmentParticipants(assignments, agents) {
3457
4995
  return Array.from(new Set([assignments.coding, assignments.qa, assignments.research]
3458
4996
  .filter((name) => Boolean(name))