@sudocode-ai/local-server 0.1.10 → 0.1.12

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 (137) hide show
  1. package/dist/execution/executors/agent-executor-wrapper.d.ts.map +1 -1
  2. package/dist/execution/executors/agent-executor-wrapper.js +57 -2
  3. package/dist/execution/executors/agent-executor-wrapper.js.map +1 -1
  4. package/dist/execution/process/builders/claude.d.ts.map +1 -1
  5. package/dist/execution/process/builders/claude.js +32 -1
  6. package/dist/execution/process/builders/claude.js.map +1 -1
  7. package/dist/execution/worktree/git-cli.d.ts +48 -0
  8. package/dist/execution/worktree/git-cli.d.ts.map +1 -1
  9. package/dist/execution/worktree/git-cli.js +81 -0
  10. package/dist/execution/worktree/git-cli.js.map +1 -1
  11. package/dist/execution/worktree/git-sync-cli.d.ts +28 -1
  12. package/dist/execution/worktree/git-sync-cli.d.ts.map +1 -1
  13. package/dist/execution/worktree/git-sync-cli.js +65 -2
  14. package/dist/execution/worktree/git-sync-cli.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +17 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/public/assets/index-CLIkhhGD.js +824 -0
  19. package/dist/public/assets/index-CLIkhhGD.js.map +1 -0
  20. package/dist/public/assets/index-p0337DGd.css +1 -0
  21. package/dist/public/assets/{react-vendor-DiL5hC7l.js → react-vendor-5f1Wq1qs.js} +5 -5
  22. package/dist/public/assets/{react-vendor-DiL5hC7l.js.map → react-vendor-5f1Wq1qs.js.map} +1 -1
  23. package/dist/public/assets/{ui-vendor-B4WMPEfa.js → ui-vendor-BDDPoYki.js} +2 -2
  24. package/dist/public/assets/{ui-vendor-B4WMPEfa.js.map → ui-vendor-BDDPoYki.js.map} +1 -1
  25. package/dist/public/index.html +4 -4
  26. package/dist/routes/workflows.d.ts +8 -0
  27. package/dist/routes/workflows.d.ts.map +1 -0
  28. package/dist/routes/workflows.js +1729 -0
  29. package/dist/routes/workflows.js.map +1 -0
  30. package/dist/services/execution-event-callbacks.d.ts +73 -0
  31. package/dist/services/execution-event-callbacks.d.ts.map +1 -0
  32. package/dist/services/execution-event-callbacks.js +82 -0
  33. package/dist/services/execution-event-callbacks.js.map +1 -0
  34. package/dist/services/execution-lifecycle.d.ts +36 -0
  35. package/dist/services/execution-lifecycle.d.ts.map +1 -1
  36. package/dist/services/execution-lifecycle.js +87 -0
  37. package/dist/services/execution-lifecycle.js.map +1 -1
  38. package/dist/services/execution-service.d.ts +31 -3
  39. package/dist/services/execution-service.d.ts.map +1 -1
  40. package/dist/services/execution-service.js +161 -34
  41. package/dist/services/execution-service.js.map +1 -1
  42. package/dist/services/executions.d.ts +1 -0
  43. package/dist/services/executions.d.ts.map +1 -1
  44. package/dist/services/executions.js +4 -0
  45. package/dist/services/executions.js.map +1 -1
  46. package/dist/services/project-context.d.ts +25 -0
  47. package/dist/services/project-context.d.ts.map +1 -1
  48. package/dist/services/project-context.js +53 -3
  49. package/dist/services/project-context.js.map +1 -1
  50. package/dist/services/project-manager.d.ts +7 -0
  51. package/dist/services/project-manager.d.ts.map +1 -1
  52. package/dist/services/project-manager.js +90 -1
  53. package/dist/services/project-manager.js.map +1 -1
  54. package/dist/services/websocket.d.ts +10 -2
  55. package/dist/services/websocket.d.ts.map +1 -1
  56. package/dist/services/websocket.js +18 -0
  57. package/dist/services/websocket.js.map +1 -1
  58. package/dist/services/workflow-broadcast-service.d.ts +43 -0
  59. package/dist/services/workflow-broadcast-service.d.ts.map +1 -0
  60. package/dist/services/workflow-broadcast-service.js +155 -0
  61. package/dist/services/workflow-broadcast-service.js.map +1 -0
  62. package/dist/services/worktree-sync-service.d.ts +40 -1
  63. package/dist/services/worktree-sync-service.d.ts.map +1 -1
  64. package/dist/services/worktree-sync-service.js +189 -16
  65. package/dist/services/worktree-sync-service.js.map +1 -1
  66. package/dist/workflow/base-workflow-engine.d.ts +186 -0
  67. package/dist/workflow/base-workflow-engine.d.ts.map +1 -0
  68. package/dist/workflow/base-workflow-engine.js +549 -0
  69. package/dist/workflow/base-workflow-engine.js.map +1 -0
  70. package/dist/workflow/dependency-analyzer.d.ts +78 -0
  71. package/dist/workflow/dependency-analyzer.d.ts.map +1 -0
  72. package/dist/workflow/dependency-analyzer.js +264 -0
  73. package/dist/workflow/dependency-analyzer.js.map +1 -0
  74. package/dist/workflow/engines/orchestrator-engine.d.ts +237 -0
  75. package/dist/workflow/engines/orchestrator-engine.d.ts.map +1 -0
  76. package/dist/workflow/engines/orchestrator-engine.js +749 -0
  77. package/dist/workflow/engines/orchestrator-engine.js.map +1 -0
  78. package/dist/workflow/engines/sequential-engine.d.ts +276 -0
  79. package/dist/workflow/engines/sequential-engine.d.ts.map +1 -0
  80. package/dist/workflow/engines/sequential-engine.js +1110 -0
  81. package/dist/workflow/engines/sequential-engine.js.map +1 -0
  82. package/dist/workflow/index.d.ts +15 -0
  83. package/dist/workflow/index.d.ts.map +1 -0
  84. package/dist/workflow/index.js +22 -0
  85. package/dist/workflow/index.js.map +1 -0
  86. package/dist/workflow/mcp/api-client.d.ts +103 -0
  87. package/dist/workflow/mcp/api-client.d.ts.map +1 -0
  88. package/dist/workflow/mcp/api-client.js +193 -0
  89. package/dist/workflow/mcp/api-client.js.map +1 -0
  90. package/dist/workflow/mcp/index.d.ts +16 -0
  91. package/dist/workflow/mcp/index.d.ts.map +1 -0
  92. package/dist/workflow/mcp/index.js +114 -0
  93. package/dist/workflow/mcp/index.js.map +1 -0
  94. package/dist/workflow/mcp/server.d.ts +85 -0
  95. package/dist/workflow/mcp/server.d.ts.map +1 -0
  96. package/dist/workflow/mcp/server.js +520 -0
  97. package/dist/workflow/mcp/server.js.map +1 -0
  98. package/dist/workflow/mcp/tools/escalation.d.ts +36 -0
  99. package/dist/workflow/mcp/tools/escalation.d.ts.map +1 -0
  100. package/dist/workflow/mcp/tools/escalation.js +47 -0
  101. package/dist/workflow/mcp/tools/escalation.js.map +1 -0
  102. package/dist/workflow/mcp/tools/execution.d.ts +59 -0
  103. package/dist/workflow/mcp/tools/execution.d.ts.map +1 -0
  104. package/dist/workflow/mcp/tools/execution.js +67 -0
  105. package/dist/workflow/mcp/tools/execution.js.map +1 -0
  106. package/dist/workflow/mcp/tools/inspection.d.ts +82 -0
  107. package/dist/workflow/mcp/tools/inspection.d.ts.map +1 -0
  108. package/dist/workflow/mcp/tools/inspection.js +57 -0
  109. package/dist/workflow/mcp/tools/inspection.js.map +1 -0
  110. package/dist/workflow/mcp/tools/workflow.d.ts +59 -0
  111. package/dist/workflow/mcp/tools/workflow.d.ts.map +1 -0
  112. package/dist/workflow/mcp/tools/workflow.js +40 -0
  113. package/dist/workflow/mcp/tools/workflow.js.map +1 -0
  114. package/dist/workflow/mcp/types.d.ts +345 -0
  115. package/dist/workflow/mcp/types.d.ts.map +1 -0
  116. package/dist/workflow/mcp/types.js +7 -0
  117. package/dist/workflow/mcp/types.js.map +1 -0
  118. package/dist/workflow/services/prompt-builder.d.ts +36 -0
  119. package/dist/workflow/services/prompt-builder.d.ts.map +1 -0
  120. package/dist/workflow/services/prompt-builder.js +329 -0
  121. package/dist/workflow/services/prompt-builder.js.map +1 -0
  122. package/dist/workflow/services/wakeup-service.d.ts +262 -0
  123. package/dist/workflow/services/wakeup-service.d.ts.map +1 -0
  124. package/dist/workflow/services/wakeup-service.js +809 -0
  125. package/dist/workflow/services/wakeup-service.js.map +1 -0
  126. package/dist/workflow/workflow-engine.d.ts +221 -0
  127. package/dist/workflow/workflow-engine.d.ts.map +1 -0
  128. package/dist/workflow/workflow-engine.js +94 -0
  129. package/dist/workflow/workflow-engine.js.map +1 -0
  130. package/dist/workflow/workflow-event-emitter.d.ts +278 -0
  131. package/dist/workflow/workflow-event-emitter.d.ts.map +1 -0
  132. package/dist/workflow/workflow-event-emitter.js +259 -0
  133. package/dist/workflow/workflow-event-emitter.js.map +1 -0
  134. package/package.json +8 -6
  135. package/dist/public/assets/index-CQoCSnhl.css +0 -1
  136. package/dist/public/assets/index-iWE3gSYw.js +0 -758
  137. package/dist/public/assets/index-iWE3gSYw.js.map +0 -1
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Orchestrator Workflow Engine
3
+ *
4
+ * Agent-managed workflow execution. The orchestrator is itself an execution
5
+ * (Claude Code agent) that controls workflow steps via MCP tools.
6
+ *
7
+ * Key differences from SequentialWorkflowEngine:
8
+ * - No internal execution loop - orchestrator agent handles execution
9
+ * - Wakeup mechanism - events trigger follow-up executions
10
+ * - MCP tools for step control - orchestrator uses workflow_* and execute_* tools
11
+ */
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ import { getIssue } from "@sudocode-ai/cli/dist/operations/issues.js";
17
+ import { BaseWorkflowEngine } from "../base-workflow-engine.js";
18
+ import { WorkflowCycleError, WorkflowStateError, WorkflowStepNotFoundError, } from "../workflow-engine.js";
19
+ import { WorkflowPromptBuilder } from "../services/prompt-builder.js";
20
+ import { registerExecutionCallback, } from "../../services/execution-event-callbacks.js";
21
+ // =============================================================================
22
+ // Orchestrator Workflow Engine
23
+ // =============================================================================
24
+ /**
25
+ * Orchestrator Workflow Engine implementation.
26
+ *
27
+ * This engine spawns an orchestrator agent (Claude Code execution) that controls
28
+ * the workflow using MCP tools. The orchestrator makes decisions about:
29
+ * - Which issues to execute
30
+ * - How to handle failures
31
+ * - When to escalate to the user
32
+ *
33
+ * Events from step executions trigger "wakeups" - follow-up messages to the
34
+ * orchestrator that inform it of completed work.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const engine = new OrchestratorWorkflowEngine({
39
+ * db,
40
+ * executionService,
41
+ * wakeupService,
42
+ * config: {
43
+ * repoPath: '/path/to/repo',
44
+ * dbPath: '/path/to/.sudocode/cache.db',
45
+ * },
46
+ * });
47
+ *
48
+ * // Create workflow from a goal
49
+ * const workflow = await engine.createWorkflow({
50
+ * type: "goal",
51
+ * goal: "Implement user authentication with OAuth"
52
+ * });
53
+ *
54
+ * // Start orchestrator
55
+ * await engine.startWorkflow(workflow.id);
56
+ * ```
57
+ */
58
+ export class OrchestratorWorkflowEngine extends BaseWorkflowEngine {
59
+ executionService;
60
+ lifecycleService;
61
+ wakeupService;
62
+ promptBuilder;
63
+ config;
64
+ unregisterExecutionCallback;
65
+ constructor(deps) {
66
+ super(deps.db, deps.eventEmitter);
67
+ this.executionService = deps.executionService;
68
+ this.lifecycleService = deps.lifecycleService;
69
+ this.wakeupService = deps.wakeupService;
70
+ this.promptBuilder = new WorkflowPromptBuilder();
71
+ this.config = deps.config;
72
+ // Register callback to record workflow events when executions complete
73
+ this.setupExecutionCallbacks();
74
+ }
75
+ // ===========================================================================
76
+ // Configuration
77
+ // ===========================================================================
78
+ /**
79
+ * Update the server URL after dynamic port discovery.
80
+ * Called by ProjectContext when the actual server port is known.
81
+ */
82
+ setServerUrl(serverUrl) {
83
+ this.config.serverUrl = serverUrl;
84
+ }
85
+ /**
86
+ * Get the wakeup service for registering await conditions.
87
+ * Used by the API endpoint for await_events MCP tool.
88
+ */
89
+ getWakeupService() {
90
+ return this.wakeupService;
91
+ }
92
+ // ===========================================================================
93
+ // Execution Event Callbacks
94
+ // ===========================================================================
95
+ /**
96
+ * Set up callbacks to record workflow events when executions complete/fail.
97
+ *
98
+ * When an execution that belongs to a workflow completes or fails, we need to:
99
+ * 1. Find the corresponding workflow step
100
+ * 2. Update the step status
101
+ * 3. Record a workflow event (step_completed or step_failed)
102
+ * 4. Trigger a wakeup so the orchestrator can react
103
+ */
104
+ setupExecutionCallbacks() {
105
+ this.unregisterExecutionCallback = registerExecutionCallback(async (event, data) => {
106
+ // Only handle events from workflow executions
107
+ if (!data.workflowId) {
108
+ return;
109
+ }
110
+ // Get the workflow
111
+ const workflow = await this.getWorkflow(data.workflowId);
112
+ if (!workflow) {
113
+ console.warn(`[OrchestratorEngine] Workflow not found for execution event: ${data.workflowId}`);
114
+ return;
115
+ }
116
+ // Find the step for this execution
117
+ const step = workflow.steps.find((s) => s.executionId === data.executionId);
118
+ if (!step) {
119
+ // Might be the orchestrator execution itself, not a step
120
+ console.debug(`[OrchestratorEngine] No step found for execution ${data.executionId} in workflow ${data.workflowId}`);
121
+ return;
122
+ }
123
+ // Clear any pending timeout for this execution
124
+ this.wakeupService.clearExecutionTimeout(data.executionId);
125
+ // Determine event type and update step status
126
+ const eventType = event === "completed" ? "step_completed" : "step_failed";
127
+ const newStepStatus = event === "completed" ? "completed" : "failed";
128
+ // Update step status in workflow
129
+ const updatedSteps = workflow.steps.map((s) => s.id === step.id
130
+ ? { ...s, status: newStepStatus }
131
+ : s);
132
+ this.updateWorkflow(data.workflowId, { steps: updatedSteps });
133
+ // Record workflow event for orchestrator
134
+ await this.wakeupService.recordEvent({
135
+ workflowId: data.workflowId,
136
+ type: eventType,
137
+ executionId: data.executionId,
138
+ stepId: step.id,
139
+ payload: {
140
+ issueId: step.issueId,
141
+ error: data.error,
142
+ },
143
+ });
144
+ // Trigger wakeup so orchestrator can react
145
+ await this.wakeupService.triggerWakeup(data.workflowId);
146
+ console.log(`[OrchestratorEngine] Recorded ${eventType} for step ${step.id} in workflow ${data.workflowId}`);
147
+ });
148
+ }
149
+ /**
150
+ * Clean up the execution callback when the engine is disposed.
151
+ */
152
+ dispose() {
153
+ if (this.unregisterExecutionCallback) {
154
+ this.unregisterExecutionCallback();
155
+ this.unregisterExecutionCallback = undefined;
156
+ }
157
+ }
158
+ // ===========================================================================
159
+ // Workflow Creation
160
+ // ===========================================================================
161
+ /**
162
+ * Create a new workflow from a source definition.
163
+ *
164
+ * For orchestrator workflows, goal-based sources create empty workflows
165
+ * that the orchestrator populates dynamically.
166
+ *
167
+ * @param source - How to determine workflow scope (spec, issues, root_issue, or goal)
168
+ * @param config - Optional configuration overrides
169
+ * @returns The created workflow
170
+ * @throws WorkflowCycleError if dependency cycles are detected
171
+ */
172
+ async createWorkflow(source, config) {
173
+ // 1. Resolve source to issue IDs
174
+ const issueIds = await this.resolveSource(source);
175
+ // 2. Handle goal-based workflows (no initial issues)
176
+ if (source.type === "goal" && issueIds.length === 0) {
177
+ const workflow = this.buildWorkflow({
178
+ source,
179
+ steps: [],
180
+ config: config || {},
181
+ repoPath: this.config.repoPath,
182
+ });
183
+ this.saveWorkflow(workflow);
184
+ return workflow;
185
+ }
186
+ // 3. Build dependency graph
187
+ const graph = this.analyzeDependencies(issueIds);
188
+ // 4. Check for cycles
189
+ if (graph.cycles && graph.cycles.length > 0) {
190
+ throw new WorkflowCycleError(graph.cycles);
191
+ }
192
+ // 5. Create steps from graph
193
+ const steps = this.createStepsFromGraph(graph);
194
+ // 6. Build workflow object
195
+ const workflow = this.buildWorkflow({
196
+ source,
197
+ steps,
198
+ config: config || {},
199
+ repoPath: this.config.repoPath,
200
+ });
201
+ // 7. Save to database
202
+ this.saveWorkflow(workflow);
203
+ return workflow;
204
+ }
205
+ // ===========================================================================
206
+ // Workflow Lifecycle
207
+ // ===========================================================================
208
+ /**
209
+ * Start executing a pending workflow.
210
+ *
211
+ * Spawns an orchestrator execution (Claude Code agent) with workflow MCP tools.
212
+ * The orchestrator will use these tools to control step execution.
213
+ *
214
+ * @param workflowId - The workflow to start
215
+ * @throws WorkflowNotFoundError if workflow doesn't exist
216
+ * @throws WorkflowStateError if workflow is not in pending state
217
+ */
218
+ async startWorkflow(workflowId) {
219
+ console.log(`[OrchestratorWorkflowEngine] startWorkflow called for ${workflowId}`);
220
+ const workflow = await this.getWorkflowOrThrow(workflowId);
221
+ // Validate state
222
+ if (workflow.status !== "pending") {
223
+ throw new WorkflowStateError(workflowId, workflow.status, "start");
224
+ }
225
+ // Create workflow-level worktree if not already present
226
+ if (!workflow.worktreePath) {
227
+ console.log(`[OrchestratorWorkflowEngine] Creating workflow worktree for ${workflowId}`);
228
+ const { worktreePath, branchName } = await this.createWorkflowWorktreeHelper(workflow, this.config.repoPath, this.lifecycleService);
229
+ // Update local workflow reference with worktree info
230
+ workflow.worktreePath = worktreePath;
231
+ workflow.branchName = branchName;
232
+ }
233
+ // Update status to running
234
+ const updated = this.updateWorkflow(workflowId, {
235
+ status: "running",
236
+ startedAt: new Date().toISOString(),
237
+ });
238
+ // Emit workflow started event
239
+ this.eventEmitter.emit({
240
+ type: "workflow_started",
241
+ workflowId,
242
+ workflow: updated,
243
+ timestamp: Date.now(),
244
+ });
245
+ // Spawn orchestrator execution
246
+ console.log(`[OrchestratorWorkflowEngine] Spawning orchestrator for workflow ${workflowId}`);
247
+ await this.spawnOrchestrator(updated);
248
+ }
249
+ /**
250
+ * Pause a running workflow.
251
+ *
252
+ * Sets pause status and records an event. The orchestrator will be notified
253
+ * via wakeup when it checks workflow status.
254
+ *
255
+ * @param workflowId - The workflow to pause
256
+ * @throws WorkflowNotFoundError if workflow doesn't exist
257
+ * @throws WorkflowStateError if workflow is not running
258
+ */
259
+ async pauseWorkflow(workflowId) {
260
+ const workflow = await this.getWorkflowOrThrow(workflowId);
261
+ if (workflow.status !== "running") {
262
+ throw new WorkflowStateError(workflowId, workflow.status, "pause");
263
+ }
264
+ // Cancel ALL running executions (orchestrator chain + step executions)
265
+ await this.cancelAllWorkflowExecutions(workflow);
266
+ // Cancel pending wakeup
267
+ this.wakeupService.cancelPendingWakeup(workflowId);
268
+ // Clear any await state
269
+ this.wakeupService.clearAwaitState(workflowId);
270
+ // Update status
271
+ this.updateWorkflow(workflowId, { status: "paused" });
272
+ // Record pause event for orchestrator
273
+ await this.wakeupService.recordEvent({
274
+ workflowId,
275
+ type: "workflow_paused",
276
+ payload: {},
277
+ });
278
+ // Emit event
279
+ this.eventEmitter.emit({
280
+ type: "workflow_paused",
281
+ workflowId,
282
+ timestamp: Date.now(),
283
+ });
284
+ }
285
+ /**
286
+ * Resume a paused workflow.
287
+ *
288
+ * Uses Claude Code's session resume to continue the orchestrator from where it left off.
289
+ * This preserves the full conversation history and context.
290
+ *
291
+ * @param workflowId - The workflow to resume
292
+ * @param message - Optional message to send with the resume (defaults to "Workflow resumed. Continue execution.")
293
+ * @throws WorkflowNotFoundError if workflow doesn't exist
294
+ * @throws WorkflowStateError if workflow is not paused
295
+ */
296
+ async resumeWorkflow(workflowId, message) {
297
+ const workflow = await this.getWorkflowOrThrow(workflowId);
298
+ if (workflow.status !== "paused") {
299
+ throw new WorkflowStateError(workflowId, workflow.status, "resume");
300
+ }
301
+ // Get the session ID to resume
302
+ const sessionId = workflow.orchestratorSessionId;
303
+ if (!sessionId) {
304
+ throw new Error(`Cannot resume workflow ${workflowId}: no orchestrator session ID found`);
305
+ }
306
+ // Update status first
307
+ this.updateWorkflow(workflowId, { status: "running" });
308
+ // Build the resume prompt
309
+ const resumePrompt = message || "Workflow resumed. Continue execution.";
310
+ // Build agent config with resume flag
311
+ const agentConfig = this.buildOrchestratorConfig(workflow);
312
+ const agentType = workflow.config.orchestratorAgentType ?? "claude-code";
313
+ console.log(`[OrchestratorWorkflowEngine] Resuming workflow ${workflowId} with session ${sessionId}`);
314
+ // Create new execution that resumes the session
315
+ // Link to the previous orchestrator execution for chain tracking
316
+ const previousExecutionId = workflow.orchestratorExecutionId;
317
+ const execution = await this.executionService.createExecution(null, // No issue - this is the orchestrator
318
+ {
319
+ mode: "worktree",
320
+ baseBranch: workflow.baseBranch,
321
+ reuseWorktreePath: workflow.worktreePath,
322
+ resume: sessionId, // Resume the previous session
323
+ parentExecutionId: previousExecutionId, // Link to previous execution in chain
324
+ ...agentConfig,
325
+ }, resumePrompt, agentType);
326
+ // Update workflow with new orchestrator execution ID
327
+ // Note: session_id should remain the same for resumed sessions
328
+ this.updateWorkflow(workflowId, {
329
+ orchestratorExecutionId: execution.id,
330
+ orchestratorSessionId: execution.session_id || sessionId,
331
+ });
332
+ // Record resume event for tracking
333
+ await this.wakeupService.recordEvent({
334
+ workflowId,
335
+ type: "workflow_resumed",
336
+ payload: {
337
+ resumedFromSession: sessionId,
338
+ previousExecutionId,
339
+ newExecutionId: execution.id,
340
+ },
341
+ });
342
+ // Mark any pending events as processed (they're now part of the resumed session context)
343
+ const pendingEvents = this.wakeupService.getUnprocessedEvents(workflowId);
344
+ if (pendingEvents.length > 0) {
345
+ this.wakeupService.markEventsProcessed(pendingEvents.map((e) => e.id));
346
+ }
347
+ // Emit event
348
+ this.eventEmitter.emit({
349
+ type: "workflow_resumed",
350
+ workflowId,
351
+ timestamp: Date.now(),
352
+ });
353
+ }
354
+ /**
355
+ * Cancel a workflow, stopping the orchestrator and any running executions.
356
+ *
357
+ * @param workflowId - The workflow to cancel
358
+ * @throws WorkflowNotFoundError if workflow doesn't exist
359
+ * @throws WorkflowStateError if workflow is already completed/failed/cancelled
360
+ */
361
+ async cancelWorkflow(workflowId) {
362
+ const workflow = await this.getWorkflowOrThrow(workflowId);
363
+ // Check if already in terminal state
364
+ if (["completed", "failed", "cancelled"].includes(workflow.status)) {
365
+ throw new WorkflowStateError(workflowId, workflow.status, "cancel");
366
+ }
367
+ // Cancel ALL running executions (orchestrator chain + step executions)
368
+ await this.cancelAllWorkflowExecutions(workflow);
369
+ // Cancel pending wakeup
370
+ this.wakeupService.cancelPendingWakeup(workflowId);
371
+ // Clear any await state
372
+ this.wakeupService.clearAwaitState(workflowId);
373
+ // Update status
374
+ this.updateWorkflow(workflowId, {
375
+ status: "cancelled",
376
+ completedAt: new Date().toISOString(),
377
+ });
378
+ // Emit event
379
+ this.eventEmitter.emit({
380
+ type: "workflow_cancelled",
381
+ workflowId,
382
+ timestamp: Date.now(),
383
+ });
384
+ }
385
+ // ===========================================================================
386
+ // Step Control
387
+ // ===========================================================================
388
+ /**
389
+ * Retry a failed step.
390
+ *
391
+ * Records an event for the orchestrator to handle.
392
+ * The orchestrator decides how to actually retry.
393
+ *
394
+ * @param workflowId - The workflow containing the step
395
+ * @param stepId - The step to retry
396
+ * @throws WorkflowNotFoundError if workflow doesn't exist
397
+ * @throws WorkflowStepNotFoundError if step doesn't exist
398
+ */
399
+ async retryStep(workflowId, stepId) {
400
+ const workflow = await this.getWorkflowOrThrow(workflowId);
401
+ const step = workflow.steps.find((s) => s.id === stepId);
402
+ if (!step) {
403
+ throw new WorkflowStepNotFoundError(workflowId, stepId);
404
+ }
405
+ // Record event for orchestrator
406
+ await this.wakeupService.recordEvent({
407
+ workflowId,
408
+ type: "step_started", // Re-use step_started as retry signal
409
+ stepId,
410
+ payload: {
411
+ action: "retry",
412
+ issueId: step.issueId,
413
+ },
414
+ });
415
+ }
416
+ /**
417
+ * Skip a step.
418
+ *
419
+ * Records an event for the orchestrator to handle.
420
+ * The orchestrator decides how to handle dependents.
421
+ *
422
+ * @param workflowId - The workflow containing the step
423
+ * @param stepId - The step to skip
424
+ * @param reason - Optional reason for skipping
425
+ * @throws WorkflowNotFoundError if workflow doesn't exist
426
+ * @throws WorkflowStepNotFoundError if step doesn't exist
427
+ */
428
+ async skipStep(workflowId, stepId, reason) {
429
+ const workflow = await this.getWorkflowOrThrow(workflowId);
430
+ const step = workflow.steps.find((s) => s.id === stepId);
431
+ if (!step) {
432
+ throw new WorkflowStepNotFoundError(workflowId, stepId);
433
+ }
434
+ // Record event for orchestrator
435
+ await this.wakeupService.recordEvent({
436
+ workflowId,
437
+ type: "step_skipped",
438
+ stepId,
439
+ payload: {
440
+ action: "skip",
441
+ issueId: step.issueId,
442
+ reason: reason || "Manually skipped",
443
+ },
444
+ });
445
+ }
446
+ // ===========================================================================
447
+ // Escalation
448
+ // ===========================================================================
449
+ /**
450
+ * Trigger a wakeup for an escalation response.
451
+ *
452
+ * Called by the API when a user responds to an escalation.
453
+ * Immediately triggers the orchestrator to resume with the response.
454
+ *
455
+ * @param workflowId - The workflow to wake up
456
+ */
457
+ async triggerEscalationWakeup(workflowId) {
458
+ await this.wakeupService.triggerWakeup(workflowId);
459
+ }
460
+ // ===========================================================================
461
+ // Private: Orchestrator Spawning
462
+ // ===========================================================================
463
+ /**
464
+ * Spawn an orchestrator execution (Claude Code agent).
465
+ *
466
+ * The orchestrator is given:
467
+ * - Workflow MCP tools for control
468
+ * - Initial prompt with workflow context
469
+ * - Access to sudocode MCP tools for issue management
470
+ */
471
+ async spawnOrchestrator(workflow) {
472
+ // Get issues for initial prompt
473
+ const issues = this.getIssuesForWorkflow(workflow);
474
+ // Build initial prompt
475
+ const prompt = this.promptBuilder.buildInitialPrompt(workflow, issues);
476
+ // Build agent config with MCP servers
477
+ const agentConfig = this.buildOrchestratorConfig(workflow);
478
+ // Determine agent type (default to claude-code)
479
+ const agentType = workflow.config.orchestratorAgentType ?? "claude-code";
480
+ // Log the full config being passed to createExecution
481
+ // Orchestrator runs in the workflow's worktree so it can see step changes
482
+ const fullConfig = {
483
+ mode: "worktree",
484
+ baseBranch: workflow.baseBranch,
485
+ reuseWorktreePath: workflow.worktreePath,
486
+ ...agentConfig,
487
+ };
488
+ console.log("[OrchestratorWorkflowEngine] Creating execution with config:", JSON.stringify(fullConfig, null, 2));
489
+ // Create orchestrator execution
490
+ const execution = await this.executionService.createExecution(null, // No issue - this is the orchestrator itself
491
+ fullConfig, prompt, agentType);
492
+ // Store orchestrator execution ID and session ID on workflow
493
+ this.updateWorkflow(workflow.id, {
494
+ orchestratorExecutionId: execution.id,
495
+ orchestratorSessionId: execution.session_id,
496
+ });
497
+ }
498
+ /**
499
+ * Build the orchestrator agent configuration.
500
+ *
501
+ * Configures MCP servers for workflow control and sudocode access.
502
+ * Enables dangerouslySkipPermissions for automated workflow execution.
503
+ */
504
+ buildOrchestratorConfig(workflow) {
505
+ const config = {
506
+ // Orchestrator runs autonomously - must skip permission prompts
507
+ dangerouslySkipPermissions: true,
508
+ };
509
+ // Add workflow MCP server
510
+ // Note: When running in dev mode (tsx/ts-node), __dirname points to src/
511
+ // but we need the compiled dist/ path. Detect and fix this.
512
+ let mcpServerPath = this.config.mcpServerPath;
513
+ if (!mcpServerPath) {
514
+ const defaultPath = path.join(__dirname, "../mcp/index.js");
515
+ // If path contains /src/, replace with /dist/ to get compiled version
516
+ mcpServerPath = defaultPath.includes("/src/")
517
+ ? defaultPath.replace("/src/", "/dist/")
518
+ : defaultPath;
519
+ }
520
+ console.log("[OrchestratorWorkflowEngine] MCP server path:", mcpServerPath);
521
+ const mcpArgs = [
522
+ mcpServerPath,
523
+ "--workflow-id",
524
+ workflow.id,
525
+ "--server-url",
526
+ this.config.serverUrl,
527
+ "--project-id",
528
+ this.config.projectId,
529
+ "--repo-path",
530
+ this.config.repoPath,
531
+ ];
532
+ config.mcpServers = {
533
+ "sudocode-workflow": {
534
+ command: "node",
535
+ args: mcpArgs,
536
+ },
537
+ };
538
+ // Log the MCP run command for local testing
539
+ console.log("[OrchestratorWorkflowEngine] MCP server config for workflow", workflow.id);
540
+ console.log("[OrchestratorWorkflowEngine] To test MCP locally, run:\n" +
541
+ ` node ${mcpArgs.join(" ")}`);
542
+ // Set model if specified
543
+ if (workflow.config.orchestratorModel) {
544
+ config.model = workflow.config.orchestratorModel;
545
+ }
546
+ // Add system prompt extension for orchestrator role
547
+ config.appendSystemPrompt = `
548
+ You are a workflow orchestrator managing the execution of coding tasks.
549
+ You have access to workflow MCP tools to control execution flow.
550
+
551
+ IMPORTANT: You are orchestrating workflow "${workflow.id}".
552
+ Use the workflow tools to:
553
+ - Check workflow status with workflow_status
554
+ - Execute issues with execute_issue
555
+ - Inspect results with execution_trajectory and execution_changes
556
+ - Handle failures appropriately based on the workflow config
557
+ - Mark the workflow complete when done with workflow_complete
558
+ `;
559
+ return config;
560
+ }
561
+ /**
562
+ * Get issues for the workflow (for initial prompt).
563
+ */
564
+ getIssuesForWorkflow(workflow) {
565
+ const issues = [];
566
+ for (const step of workflow.steps) {
567
+ const issue = getIssue(this.db, step.issueId);
568
+ if (issue) {
569
+ issues.push(issue);
570
+ }
571
+ }
572
+ return issues;
573
+ }
574
+ /**
575
+ * Cancel ALL running executions for a workflow.
576
+ * This includes:
577
+ * 1. The entire orchestrator execution chain (root + all follow-ups)
578
+ * 2. All step executions linked to this workflow
579
+ */
580
+ async cancelAllWorkflowExecutions(workflow) {
581
+ const cancelledIds = new Set();
582
+ // 1. Cancel the entire orchestrator execution chain
583
+ if (workflow.orchestratorExecutionId) {
584
+ // Get the root orchestrator execution by traversing up parent_execution_id
585
+ let rootId = workflow.orchestratorExecutionId;
586
+ let safetyCounter = 100; // Prevent infinite loops
587
+ while (safetyCounter > 0) {
588
+ const parent = this.db
589
+ .prepare("SELECT parent_execution_id FROM executions WHERE id = ?")
590
+ .get(rootId);
591
+ if (!parent || !parent.parent_execution_id) {
592
+ break;
593
+ }
594
+ rootId = parent.parent_execution_id;
595
+ safetyCounter--;
596
+ }
597
+ // Get all executions in the chain (root + all descendants)
598
+ const chainExecutions = this.db
599
+ .prepare(`
600
+ WITH RECURSIVE execution_chain AS (
601
+ -- Base case: the root execution
602
+ SELECT id, status FROM executions WHERE id = ?
603
+ UNION ALL
604
+ -- Recursive case: children of executions in the chain
605
+ SELECT e.id, e.status FROM executions e
606
+ INNER JOIN execution_chain ec ON e.parent_execution_id = ec.id
607
+ )
608
+ SELECT id, status FROM execution_chain
609
+ WHERE status IN ('pending', 'running', 'preparing')
610
+ `)
611
+ .all(rootId);
612
+ console.log(`[OrchestratorEngine] Cancelling ${chainExecutions.length} orchestrator executions in chain`);
613
+ for (const { id } of chainExecutions) {
614
+ if (cancelledIds.has(id))
615
+ continue;
616
+ try {
617
+ await this.executionService.cancelExecution(id);
618
+ cancelledIds.add(id);
619
+ }
620
+ catch (error) {
621
+ console.warn(`Failed to cancel orchestrator execution ${id}:`, error);
622
+ }
623
+ }
624
+ }
625
+ // 2. Cancel all step executions linked to this workflow
626
+ const stepExecutions = this.db
627
+ .prepare(`
628
+ SELECT id FROM executions
629
+ WHERE workflow_execution_id = ?
630
+ AND status IN ('pending', 'running', 'preparing')
631
+ `)
632
+ .all(workflow.id);
633
+ console.log(`[OrchestratorEngine] Cancelling ${stepExecutions.length} step executions`);
634
+ for (const { id } of stepExecutions) {
635
+ if (cancelledIds.has(id))
636
+ continue;
637
+ try {
638
+ await this.executionService.cancelExecution(id);
639
+ cancelledIds.add(id);
640
+ }
641
+ catch (error) {
642
+ console.warn(`Failed to cancel step execution ${id}:`, error);
643
+ }
644
+ }
645
+ console.log(`[OrchestratorEngine] Cancelled ${cancelledIds.size} total executions for workflow ${workflow.id}`);
646
+ }
647
+ // ===========================================================================
648
+ // Recovery
649
+ // ===========================================================================
650
+ /**
651
+ * Recover orphaned workflows on server restart.
652
+ *
653
+ * Finds workflows in 'running' status whose orchestrator execution
654
+ * is no longer running, and triggers a wakeup to resume them.
655
+ */
656
+ async recoverOrphanedWorkflows() {
657
+ console.log("[OrchestratorEngine] Checking for orphaned workflows...");
658
+ // Find workflows in 'running' status
659
+ const runningWorkflows = this.db
660
+ .prepare(`SELECT * FROM workflows WHERE status = 'running'`)
661
+ .all();
662
+ let recoveredCount = 0;
663
+ for (const row of runningWorkflows) {
664
+ // Parse the workflow row
665
+ const workflow = {
666
+ id: row.id,
667
+ title: row.title,
668
+ source: JSON.parse(row.source),
669
+ status: row.status,
670
+ steps: JSON.parse(row.steps),
671
+ worktreePath: row.worktree_path ?? undefined,
672
+ branchName: row.branch_name ?? undefined,
673
+ baseBranch: row.base_branch,
674
+ currentStepIndex: row.current_step_index,
675
+ orchestratorExecutionId: row.orchestrator_execution_id ?? undefined,
676
+ orchestratorSessionId: row.orchestrator_session_id ?? undefined,
677
+ config: JSON.parse(row.config),
678
+ createdAt: row.created_at,
679
+ updatedAt: row.updated_at,
680
+ startedAt: row.started_at ?? undefined,
681
+ completedAt: row.completed_at ?? undefined,
682
+ };
683
+ // Skip if no orchestrator execution
684
+ if (!workflow.orchestratorExecutionId) {
685
+ console.warn(`[OrchestratorEngine] Workflow ${workflow.id} running but no orchestrator`);
686
+ continue;
687
+ }
688
+ // Check orchestrator execution status
689
+ const orchestrator = this.db
690
+ .prepare(`SELECT status FROM executions WHERE id = ?`)
691
+ .get(workflow.orchestratorExecutionId);
692
+ // If orchestrator is not running, trigger recovery
693
+ if (!orchestrator || orchestrator.status !== "running") {
694
+ console.log(`[OrchestratorEngine] Recovering workflow ${workflow.id} ` +
695
+ `(orchestrator status: ${orchestrator?.status ?? "not found"})`);
696
+ try {
697
+ // Record recovery event
698
+ await this.wakeupService.recordEvent({
699
+ workflowId: workflow.id,
700
+ type: "orchestrator_wakeup",
701
+ payload: {
702
+ reason: "recovery",
703
+ previousStatus: orchestrator?.status,
704
+ },
705
+ });
706
+ // Trigger wakeup to resume
707
+ await this.wakeupService.triggerWakeup(workflow.id);
708
+ recoveredCount++;
709
+ }
710
+ catch (err) {
711
+ console.error(`[OrchestratorEngine] Failed to recover workflow ${workflow.id}:`, err);
712
+ }
713
+ }
714
+ }
715
+ console.log(`[OrchestratorEngine] Recovery complete: ${recoveredCount}/${runningWorkflows.length} workflows recovered`);
716
+ }
717
+ /**
718
+ * Mark stale running executions as failed.
719
+ *
720
+ * Called during recovery to clean up executions that were running
721
+ * when the server crashed.
722
+ */
723
+ async markStaleExecutionsAsFailed() {
724
+ console.log("[OrchestratorEngine] Checking for stale executions...");
725
+ const staleExecutions = this.db
726
+ .prepare(`
727
+ SELECT id, workflow_execution_id FROM executions
728
+ WHERE status = 'running'
729
+ AND workflow_execution_id IS NOT NULL
730
+ `)
731
+ .all();
732
+ for (const exec of staleExecutions) {
733
+ console.log(`[OrchestratorEngine] Marking stale execution ${exec.id} as failed`);
734
+ this.db
735
+ .prepare(`
736
+ UPDATE executions
737
+ SET status = 'failed',
738
+ error_message = 'Execution was running when server restarted',
739
+ completed_at = ?
740
+ WHERE id = ?
741
+ `)
742
+ .run(new Date().toISOString(), exec.id);
743
+ }
744
+ if (staleExecutions.length > 0) {
745
+ console.log(`[OrchestratorEngine] Marked ${staleExecutions.length} stale executions as failed`);
746
+ }
747
+ }
748
+ }
749
+ //# sourceMappingURL=orchestrator-engine.js.map