@sudocode-ai/local-server 0.1.7 → 0.1.9

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 (282) hide show
  1. package/README.md +6 -0
  2. package/dist/errors/agent-errors.d.ts +43 -0
  3. package/dist/errors/agent-errors.d.ts.map +1 -0
  4. package/dist/errors/agent-errors.js +69 -0
  5. package/dist/errors/agent-errors.js.map +1 -0
  6. package/dist/execution/adapters/claude-adapter.d.ts +63 -0
  7. package/dist/execution/adapters/claude-adapter.d.ts.map +1 -0
  8. package/dist/execution/adapters/claude-adapter.js +82 -0
  9. package/dist/execution/adapters/claude-adapter.js.map +1 -0
  10. package/dist/execution/adapters/codex-adapter.d.ts +67 -0
  11. package/dist/execution/adapters/codex-adapter.d.ts.map +1 -0
  12. package/dist/execution/adapters/codex-adapter.js +183 -0
  13. package/dist/execution/adapters/codex-adapter.js.map +1 -0
  14. package/dist/execution/adapters/codex-config-builder.d.ts +30 -0
  15. package/dist/execution/adapters/codex-config-builder.d.ts.map +1 -0
  16. package/dist/execution/adapters/codex-config-builder.js +110 -0
  17. package/dist/execution/adapters/codex-config-builder.js.map +1 -0
  18. package/dist/execution/adapters/copilot-adapter.d.ts +94 -0
  19. package/dist/execution/adapters/copilot-adapter.d.ts.map +1 -0
  20. package/dist/execution/adapters/copilot-adapter.js +163 -0
  21. package/dist/execution/adapters/copilot-adapter.js.map +1 -0
  22. package/dist/execution/adapters/copilot-config-builder.d.ts +48 -0
  23. package/dist/execution/adapters/copilot-config-builder.d.ts.map +1 -0
  24. package/dist/execution/adapters/copilot-config-builder.js +125 -0
  25. package/dist/execution/adapters/copilot-config-builder.js.map +1 -0
  26. package/dist/execution/adapters/cursor-adapter.d.ts +66 -0
  27. package/dist/execution/adapters/cursor-adapter.d.ts.map +1 -0
  28. package/dist/execution/adapters/cursor-adapter.js +121 -0
  29. package/dist/execution/adapters/cursor-adapter.js.map +1 -0
  30. package/dist/execution/adapters/cursor-config-builder.d.ts +29 -0
  31. package/dist/execution/adapters/cursor-config-builder.d.ts.map +1 -0
  32. package/dist/execution/adapters/cursor-config-builder.js +49 -0
  33. package/dist/execution/adapters/cursor-config-builder.js.map +1 -0
  34. package/dist/execution/adapters/shared/config-presets.d.ts +102 -0
  35. package/dist/execution/adapters/shared/config-presets.d.ts.map +1 -0
  36. package/dist/execution/adapters/shared/config-presets.js +205 -0
  37. package/dist/execution/adapters/shared/config-presets.js.map +1 -0
  38. package/dist/execution/adapters/shared/config-utils.d.ts +95 -0
  39. package/dist/execution/adapters/shared/config-utils.d.ts.map +1 -0
  40. package/dist/execution/adapters/shared/config-utils.js +163 -0
  41. package/dist/execution/adapters/shared/config-utils.js.map +1 -0
  42. package/dist/execution/adapters/shared/index.d.ts +8 -0
  43. package/dist/execution/adapters/shared/index.d.ts.map +1 -0
  44. package/dist/execution/adapters/shared/index.js +8 -0
  45. package/dist/execution/adapters/shared/index.js.map +1 -0
  46. package/dist/execution/executors/agent-executor-wrapper.d.ts +153 -0
  47. package/dist/execution/executors/agent-executor-wrapper.d.ts.map +1 -0
  48. package/dist/execution/executors/agent-executor-wrapper.js +652 -0
  49. package/dist/execution/executors/agent-executor-wrapper.js.map +1 -0
  50. package/dist/execution/executors/executor-factory.d.ts +95 -0
  51. package/dist/execution/executors/executor-factory.d.ts.map +1 -0
  52. package/dist/execution/executors/executor-factory.js +120 -0
  53. package/dist/execution/executors/executor-factory.js.map +1 -0
  54. package/dist/execution/output/ag-ui-adapter.d.ts +0 -2
  55. package/dist/execution/output/ag-ui-adapter.d.ts.map +1 -1
  56. package/dist/execution/output/ag-ui-adapter.js +0 -2
  57. package/dist/execution/output/ag-ui-adapter.js.map +1 -1
  58. package/dist/execution/output/index.d.ts +0 -3
  59. package/dist/execution/output/index.d.ts.map +1 -1
  60. package/dist/execution/output/index.js +0 -2
  61. package/dist/execution/output/index.js.map +1 -1
  62. package/dist/execution/output/normalized-to-ag-ui-adapter.d.ts +108 -0
  63. package/dist/execution/output/normalized-to-ag-ui-adapter.d.ts.map +1 -0
  64. package/dist/execution/output/normalized-to-ag-ui-adapter.js +321 -0
  65. package/dist/execution/output/normalized-to-ag-ui-adapter.js.map +1 -0
  66. package/dist/execution/process/builders/claude.d.ts +24 -57
  67. package/dist/execution/process/builders/claude.d.ts.map +1 -1
  68. package/dist/execution/process/builders/claude.js +153 -19
  69. package/dist/execution/process/builders/claude.js.map +1 -1
  70. package/dist/execution/transport/ipc-transport-manager.d.ts +74 -0
  71. package/dist/execution/transport/ipc-transport-manager.d.ts.map +1 -0
  72. package/dist/execution/transport/ipc-transport-manager.js +104 -0
  73. package/dist/execution/transport/ipc-transport-manager.js.map +1 -0
  74. package/dist/execution/transport/transport-manager.d.ts.map +1 -1
  75. package/dist/execution/transport/transport-manager.js +3 -0
  76. package/dist/execution/transport/transport-manager.js.map +1 -1
  77. package/dist/execution/worktree/conflict-detector.d.ts +85 -0
  78. package/dist/execution/worktree/conflict-detector.d.ts.map +1 -0
  79. package/dist/execution/worktree/conflict-detector.js +129 -0
  80. package/dist/execution/worktree/conflict-detector.js.map +1 -0
  81. package/dist/execution/worktree/git-cli.d.ts +9 -0
  82. package/dist/execution/worktree/git-cli.d.ts.map +1 -1
  83. package/dist/execution/worktree/git-cli.js +10 -0
  84. package/dist/execution/worktree/git-cli.js.map +1 -1
  85. package/dist/execution/worktree/git-sync-cli.d.ts +198 -0
  86. package/dist/execution/worktree/git-sync-cli.d.ts.map +1 -0
  87. package/dist/execution/worktree/git-sync-cli.js +401 -0
  88. package/dist/execution/worktree/git-sync-cli.js.map +1 -0
  89. package/dist/execution/worktree/manager.d.ts +18 -0
  90. package/dist/execution/worktree/manager.d.ts.map +1 -1
  91. package/dist/execution/worktree/manager.js +9 -3
  92. package/dist/execution/worktree/manager.js.map +1 -1
  93. package/dist/index.d.ts +1 -3
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +124 -229
  96. package/dist/index.js.map +1 -1
  97. package/dist/middleware/project-context.d.ts +37 -0
  98. package/dist/middleware/project-context.d.ts.map +1 -0
  99. package/dist/middleware/project-context.js +91 -0
  100. package/dist/middleware/project-context.js.map +1 -0
  101. package/dist/public/assets/index-DV9Tbujb.css +1 -0
  102. package/dist/public/assets/index-DcDX9-Ad.js +740 -0
  103. package/dist/public/assets/index-DcDX9-Ad.js.map +1 -0
  104. package/dist/public/assets/{react-vendor-ByUx1V_q.js → react-vendor-DiL5hC7l.js} +2 -2
  105. package/dist/public/assets/{react-vendor-ByUx1V_q.js.map → react-vendor-DiL5hC7l.js.map} +1 -1
  106. package/dist/public/assets/ui-vendor-B4WMPEfa.js +54 -0
  107. package/dist/public/assets/ui-vendor-B4WMPEfa.js.map +1 -0
  108. package/dist/public/index.html +4 -4
  109. package/dist/routes/agents.d.ts +3 -0
  110. package/dist/routes/agents.d.ts.map +1 -0
  111. package/dist/routes/agents.js +62 -0
  112. package/dist/routes/agents.js.map +1 -0
  113. package/dist/routes/config.d.ts +3 -0
  114. package/dist/routes/config.d.ts.map +1 -0
  115. package/dist/routes/config.js +25 -0
  116. package/dist/routes/config.js.map +1 -0
  117. package/dist/routes/editors.d.ts +15 -0
  118. package/dist/routes/editors.d.ts.map +1 -0
  119. package/dist/routes/editors.js +98 -0
  120. package/dist/routes/editors.js.map +1 -0
  121. package/dist/routes/executions-stream.d.ts +8 -5
  122. package/dist/routes/executions-stream.d.ts.map +1 -1
  123. package/dist/routes/executions-stream.js +10 -6
  124. package/dist/routes/executions-stream.js.map +1 -1
  125. package/dist/routes/executions.d.ts +6 -10
  126. package/dist/routes/executions.d.ts.map +1 -1
  127. package/dist/routes/executions.js +792 -37
  128. package/dist/routes/executions.js.map +1 -1
  129. package/dist/routes/feedback.d.ts +3 -2
  130. package/dist/routes/feedback.d.ts.map +1 -1
  131. package/dist/routes/feedback.js +12 -10
  132. package/dist/routes/feedback.js.map +1 -1
  133. package/dist/routes/files.d.ts +18 -0
  134. package/dist/routes/files.d.ts.map +1 -0
  135. package/dist/routes/files.js +89 -0
  136. package/dist/routes/files.js.map +1 -0
  137. package/dist/routes/issues.d.ts +3 -2
  138. package/dist/routes/issues.d.ts.map +1 -1
  139. package/dist/routes/issues.js +19 -18
  140. package/dist/routes/issues.js.map +1 -1
  141. package/dist/routes/projects.d.ts +11 -0
  142. package/dist/routes/projects.d.ts.map +1 -0
  143. package/dist/routes/projects.js +447 -0
  144. package/dist/routes/projects.js.map +1 -0
  145. package/dist/routes/relationships.d.ts +3 -2
  146. package/dist/routes/relationships.d.ts.map +1 -1
  147. package/dist/routes/relationships.js +12 -10
  148. package/dist/routes/relationships.js.map +1 -1
  149. package/dist/routes/repo-info.d.ts +3 -0
  150. package/dist/routes/repo-info.d.ts.map +1 -0
  151. package/dist/routes/repo-info.js +203 -0
  152. package/dist/routes/repo-info.js.map +1 -0
  153. package/dist/routes/specs.d.ts +3 -2
  154. package/dist/routes/specs.d.ts.map +1 -1
  155. package/dist/routes/specs.js +19 -18
  156. package/dist/routes/specs.js.map +1 -1
  157. package/dist/routes/version.d.ts +3 -0
  158. package/dist/routes/version.d.ts.map +1 -0
  159. package/dist/routes/version.js +25 -0
  160. package/dist/routes/version.js.map +1 -0
  161. package/dist/services/agent-registry.d.ts +140 -0
  162. package/dist/services/agent-registry.d.ts.map +1 -0
  163. package/dist/services/agent-registry.js +272 -0
  164. package/dist/services/agent-registry.js.map +1 -0
  165. package/dist/services/editor-service.d.ts +57 -0
  166. package/dist/services/editor-service.d.ts.map +1 -0
  167. package/dist/services/editor-service.js +204 -0
  168. package/dist/services/editor-service.js.map +1 -0
  169. package/dist/services/execution-changes-service.d.ts +110 -0
  170. package/dist/services/execution-changes-service.d.ts.map +1 -0
  171. package/dist/services/execution-changes-service.js +700 -0
  172. package/dist/services/execution-changes-service.js.map +1 -0
  173. package/dist/services/execution-lifecycle.d.ts +1 -0
  174. package/dist/services/execution-lifecycle.d.ts.map +1 -1
  175. package/dist/services/execution-lifecycle.js +37 -7
  176. package/dist/services/execution-lifecycle.js.map +1 -1
  177. package/dist/services/execution-logs-store.d.ts +75 -0
  178. package/dist/services/execution-logs-store.d.ts.map +1 -1
  179. package/dist/services/execution-logs-store.js +142 -2
  180. package/dist/services/execution-logs-store.js.map +1 -1
  181. package/dist/services/execution-service.d.ts +82 -59
  182. package/dist/services/execution-service.d.ts.map +1 -1
  183. package/dist/services/execution-service.js +514 -469
  184. package/dist/services/execution-service.js.map +1 -1
  185. package/dist/services/execution-worker-pool.d.ts +116 -0
  186. package/dist/services/execution-worker-pool.d.ts.map +1 -0
  187. package/dist/services/execution-worker-pool.js +326 -0
  188. package/dist/services/execution-worker-pool.js.map +1 -0
  189. package/dist/services/executions.d.ts +3 -0
  190. package/dist/services/executions.d.ts.map +1 -1
  191. package/dist/services/executions.js +11 -17
  192. package/dist/services/executions.js.map +1 -1
  193. package/dist/services/export.d.ts +8 -2
  194. package/dist/services/export.d.ts.map +1 -1
  195. package/dist/services/export.js +29 -23
  196. package/dist/services/export.js.map +1 -1
  197. package/dist/services/file-search/git-ls-files-strategy.d.ts +72 -0
  198. package/dist/services/file-search/git-ls-files-strategy.d.ts.map +1 -0
  199. package/dist/services/file-search/git-ls-files-strategy.js +176 -0
  200. package/dist/services/file-search/git-ls-files-strategy.js.map +1 -0
  201. package/dist/services/file-search/index.d.ts +9 -0
  202. package/dist/services/file-search/index.d.ts.map +1 -0
  203. package/dist/services/file-search/index.js +10 -0
  204. package/dist/services/file-search/index.js.map +1 -0
  205. package/dist/services/file-search/registry.d.ts +97 -0
  206. package/dist/services/file-search/registry.d.ts.map +1 -0
  207. package/dist/services/file-search/registry.js +140 -0
  208. package/dist/services/file-search/registry.js.map +1 -0
  209. package/dist/services/file-search/strategy.d.ts +58 -0
  210. package/dist/services/file-search/strategy.d.ts.map +1 -0
  211. package/dist/services/file-search/strategy.js +8 -0
  212. package/dist/services/file-search/strategy.js.map +1 -0
  213. package/dist/services/project-context.d.ts +69 -0
  214. package/dist/services/project-context.d.ts.map +1 -0
  215. package/dist/services/project-context.js +113 -0
  216. package/dist/services/project-context.js.map +1 -0
  217. package/dist/services/project-manager.d.ts +95 -0
  218. package/dist/services/project-manager.d.ts.map +1 -0
  219. package/dist/services/project-manager.js +388 -0
  220. package/dist/services/project-manager.js.map +1 -0
  221. package/dist/services/project-registry.d.ts +98 -0
  222. package/dist/services/project-registry.d.ts.map +1 -0
  223. package/dist/services/project-registry.js +289 -0
  224. package/dist/services/project-registry.js.map +1 -0
  225. package/dist/services/prompt-resolver.d.ts +97 -0
  226. package/dist/services/prompt-resolver.d.ts.map +1 -0
  227. package/dist/services/prompt-resolver.js +377 -0
  228. package/dist/services/prompt-resolver.js.map +1 -0
  229. package/dist/services/repo-info.d.ts +12 -0
  230. package/dist/services/repo-info.d.ts.map +1 -1
  231. package/dist/services/repo-info.js +46 -0
  232. package/dist/services/repo-info.js.map +1 -1
  233. package/dist/services/version-service.d.ts +14 -0
  234. package/dist/services/version-service.d.ts.map +1 -0
  235. package/dist/services/version-service.js +57 -0
  236. package/dist/services/version-service.js.map +1 -0
  237. package/dist/services/watcher.d.ts +3 -4
  238. package/dist/services/watcher.d.ts.map +1 -1
  239. package/dist/services/watcher.js +18 -35
  240. package/dist/services/watcher.js.map +1 -1
  241. package/dist/services/websocket.d.ts +30 -16
  242. package/dist/services/websocket.d.ts.map +1 -1
  243. package/dist/services/websocket.js +102 -37
  244. package/dist/services/websocket.js.map +1 -1
  245. package/dist/services/worktree-sync-service.d.ts +326 -0
  246. package/dist/services/worktree-sync-service.d.ts.map +1 -0
  247. package/dist/services/worktree-sync-service.js +1091 -0
  248. package/dist/services/worktree-sync-service.js.map +1 -0
  249. package/dist/types/editor.d.ts +49 -0
  250. package/dist/types/editor.d.ts.map +1 -0
  251. package/dist/types/editor.js +50 -0
  252. package/dist/types/editor.js.map +1 -0
  253. package/dist/types/project.d.ts +58 -0
  254. package/dist/types/project.d.ts.map +1 -0
  255. package/dist/types/project.js +10 -0
  256. package/dist/types/project.js.map +1 -0
  257. package/dist/utils/executable-check.d.ts +36 -0
  258. package/dist/utils/executable-check.d.ts.map +1 -0
  259. package/dist/utils/executable-check.js +79 -0
  260. package/dist/utils/executable-check.js.map +1 -0
  261. package/dist/workers/execution-worker.d.ts +18 -0
  262. package/dist/workers/execution-worker.d.ts.map +1 -0
  263. package/dist/workers/execution-worker.js +340 -0
  264. package/dist/workers/execution-worker.js.map +1 -0
  265. package/dist/workers/worker-ipc.d.ts +84 -0
  266. package/dist/workers/worker-ipc.d.ts.map +1 -0
  267. package/dist/workers/worker-ipc.js +29 -0
  268. package/dist/workers/worker-ipc.js.map +1 -0
  269. package/package.json +6 -5
  270. package/dist/execution/output/ag-ui-integration.d.ts +0 -96
  271. package/dist/execution/output/ag-ui-integration.d.ts.map +0 -1
  272. package/dist/execution/output/ag-ui-integration.js +0 -96
  273. package/dist/execution/output/ag-ui-integration.js.map +0 -1
  274. package/dist/execution/output/claude-code-output-processor.d.ts +0 -321
  275. package/dist/execution/output/claude-code-output-processor.d.ts.map +0 -1
  276. package/dist/execution/output/claude-code-output-processor.js +0 -769
  277. package/dist/execution/output/claude-code-output-processor.js.map +0 -1
  278. package/dist/public/assets/index-B3SEMufD.js +0 -580
  279. package/dist/public/assets/index-B3SEMufD.js.map +0 -1
  280. package/dist/public/assets/index-D2YGL3gX.css +0 -1
  281. package/dist/public/assets/ui-vendor-CotR6bx9.js +0 -54
  282. package/dist/public/assets/ui-vendor-CotR6bx9.js.map +0 -1
@@ -6,137 +6,50 @@
6
6
  *
7
7
  * @module services/execution-service
8
8
  */
9
- import { PromptTemplateEngine } from "./prompt-template-engine.js";
10
9
  import { ExecutionLifecycleService } from "./execution-lifecycle.js";
11
10
  import { createExecution, getExecution, updateExecution, } from "./executions.js";
12
- import { getDefaultTemplate, getTemplateById } from "./prompt-templates.js";
13
11
  import { randomUUID } from "crypto";
14
- import { SimpleProcessManager, SimpleExecutionEngine, ResilientExecutor, LinearOrchestrator, } from "agent-execution-engine";
15
- import { createAgUiSystem } from "../execution/output/ag-ui-integration.js";
12
+ import { execSync } from "child_process";
16
13
  import { ExecutionLogsStore } from "./execution-logs-store.js";
14
+ import { broadcastExecutionUpdate } from "./websocket.js";
15
+ import { createExecutorForAgent } from "../execution/executors/executor-factory.js";
16
+ import { PromptResolver } from "./prompt-resolver.js";
17
17
  /**
18
18
  * ExecutionService
19
19
  *
20
20
  * Manages the full lifecycle of issue-based executions:
21
- * - Preparing execution with template rendering
22
21
  * - Creating and starting executions with worktree isolation
23
22
  * - Creating follow-up executions that reuse worktrees
24
23
  * - Canceling and cleaning up executions
25
24
  */
26
25
  export class ExecutionService {
27
26
  db;
28
- templateEngine;
27
+ projectId;
29
28
  lifecycleService;
30
29
  repoPath;
31
30
  transportManager;
32
31
  logsStore;
33
- activeOrchestrators = new Map();
32
+ workerPool;
34
33
  /**
35
34
  * Create a new ExecutionService
36
35
  *
37
36
  * @param db - Database instance
37
+ * @param projectId - Project ID for WebSocket broadcasts
38
38
  * @param repoPath - Path to the git repository
39
39
  * @param lifecycleService - Optional execution lifecycle service (creates one if not provided)
40
40
  * @param transportManager - Optional transport manager for SSE streaming
41
41
  * @param logsStore - Optional execution logs store (creates one if not provided)
42
+ * @param workerPool - Optional worker pool for isolated execution processes
42
43
  */
43
- constructor(db, repoPath, lifecycleService, transportManager, logsStore) {
44
+ constructor(db, projectId, repoPath, lifecycleService, transportManager, logsStore, workerPool) {
44
45
  this.db = db;
46
+ this.projectId = projectId;
45
47
  this.repoPath = repoPath;
46
- this.templateEngine = new PromptTemplateEngine();
47
48
  this.lifecycleService =
48
49
  lifecycleService || new ExecutionLifecycleService(db, repoPath);
49
50
  this.transportManager = transportManager;
50
51
  this.logsStore = logsStore || new ExecutionLogsStore(db);
51
- }
52
- /**
53
- * Prepare execution - load issue, render template, return preview
54
- *
55
- * This method loads the issue and related context, renders the template,
56
- * and returns a preview for the user to review before starting execution.
57
- *
58
- * @param issueId - ID of issue to prepare execution for
59
- * @param options - Optional template and config overrides
60
- * @returns Execution prepare result with rendered prompt and context
61
- */
62
- async prepareExecution(issueId, options) {
63
- // 1. Load issue
64
- const issue = this.db
65
- .prepare("SELECT * FROM issues WHERE id = ?")
66
- .get(issueId);
67
- if (!issue) {
68
- throw new Error(`Issue ${issueId} not found`);
69
- }
70
- // 2. Load related specs (via implements/references relationships)
71
- const relatedSpecs = this.db
72
- .prepare(`
73
- SELECT DISTINCT s.id, s.title
74
- FROM specs s
75
- JOIN relationships r ON r.to_id = s.id AND r.to_type = 'spec'
76
- WHERE r.from_id = ? AND r.from_type = 'issue'
77
- AND r.relationship_type IN ('implements', 'references')
78
- ORDER BY s.title
79
- `)
80
- .all(issueId);
81
- // 3. Build context for template rendering
82
- const context = {
83
- issueId: issue.id,
84
- title: issue.title,
85
- description: issue.content,
86
- relatedSpecs: relatedSpecs.length > 0
87
- ? relatedSpecs.map((s) => ({
88
- id: s.id,
89
- title: s.title,
90
- }))
91
- : undefined,
92
- };
93
- // 4. Get template (use custom template if provided, otherwise default)
94
- let template;
95
- if (options?.templateId) {
96
- const customTemplate = getTemplateById(this.db, options.templateId);
97
- if (!customTemplate) {
98
- throw new Error(`Template ${options.templateId} not found`);
99
- }
100
- template = customTemplate.template;
101
- }
102
- else {
103
- const defaultTemplate = getDefaultTemplate(this.db, "issue");
104
- if (!defaultTemplate) {
105
- throw new Error("Default issue template not found");
106
- }
107
- template = defaultTemplate.template;
108
- }
109
- // 5. Render template
110
- const renderedPrompt = this.templateEngine.render(template, context);
111
- // 6. Get default config
112
- const defaultConfig = {
113
- mode: "worktree",
114
- model: "claude-sonnet-4",
115
- baseBranch: "main",
116
- checkpointInterval: 1,
117
- continueOnStepFailure: false,
118
- captureFileChanges: true,
119
- captureToolCalls: true,
120
- ...options?.config,
121
- };
122
- // 7. Validate
123
- const warnings = [];
124
- const errors = [];
125
- if (!renderedPrompt.trim()) {
126
- errors.push("Rendered prompt is empty");
127
- }
128
- return {
129
- renderedPrompt,
130
- issue: {
131
- id: issue.id,
132
- title: issue.title,
133
- content: issue.content,
134
- },
135
- relatedSpecs,
136
- defaultConfig,
137
- warnings,
138
- errors,
139
- };
52
+ this.workerPool = workerPool;
140
53
  }
141
54
  /**
142
55
  * Create and start execution
@@ -148,9 +61,10 @@ export class ExecutionService {
148
61
  * @param issueId - ID of issue to execute
149
62
  * @param config - Execution configuration
150
63
  * @param prompt - Rendered prompt to execute
64
+ * @param agentType - Type of agent to use (defaults to 'claude-code')
151
65
  * @returns Created execution record
152
66
  */
153
- async createExecution(issueId, config, prompt) {
67
+ async createExecution(issueId, config, prompt, agentType = "claude-code") {
154
68
  // 1. Validate
155
69
  if (!prompt.trim()) {
156
70
  throw new Error("Prompt cannot be empty");
@@ -162,23 +76,51 @@ export class ExecutionService {
162
76
  throw new Error(`Issue ${issueId} not found`);
163
77
  }
164
78
  // 2. Determine execution mode and create execution with worktree
79
+ // Store the original (unexpanded) prompt in the database
165
80
  const mode = config.mode || "worktree";
166
81
  let execution;
167
82
  let workDir;
168
83
  if (mode === "worktree") {
169
- // Create execution with isolated worktree
170
- const result = await this.lifecycleService.createExecutionWithWorktree({
171
- issueId,
172
- issueTitle: issue.title,
173
- agentType: "claude-code",
174
- targetBranch: config.baseBranch || "main",
175
- repoPath: this.repoPath,
176
- mode: mode,
177
- prompt: prompt,
178
- config: JSON.stringify(config),
179
- });
180
- execution = result.execution;
181
- workDir = result.worktreePath;
84
+ // Check if we're reusing an existing worktree
85
+ if (config.reuseWorktreeId) {
86
+ // Reuse existing worktree
87
+ const existingExecution = this.db
88
+ .prepare("SELECT * FROM executions WHERE id = ?")
89
+ .get(config.reuseWorktreeId);
90
+ if (!existingExecution || !existingExecution.worktree_path) {
91
+ throw new Error(`Cannot reuse worktree: execution ${config.reuseWorktreeId} not found or has no worktree`);
92
+ }
93
+ // Create execution record with the same worktree path
94
+ const executionId = randomUUID();
95
+ execution = createExecution(this.db, {
96
+ id: executionId,
97
+ issue_id: issueId,
98
+ agent_type: agentType,
99
+ mode: mode,
100
+ prompt: prompt,
101
+ config: JSON.stringify(config),
102
+ target_branch: existingExecution.target_branch,
103
+ branch_name: existingExecution.branch_name,
104
+ worktree_path: existingExecution.worktree_path, // Reuse the same worktree path
105
+ });
106
+ workDir = existingExecution.worktree_path;
107
+ }
108
+ else {
109
+ // Create execution with isolated worktree
110
+ const result = await this.lifecycleService.createExecutionWithWorktree({
111
+ issueId,
112
+ issueTitle: issue.title,
113
+ agentType: agentType,
114
+ targetBranch: config.baseBranch || "main",
115
+ repoPath: this.repoPath,
116
+ mode: mode,
117
+ prompt: prompt, // Store original (unexpanded) prompt
118
+ config: JSON.stringify(config),
119
+ createTargetBranch: config.createBaseBranch || false,
120
+ });
121
+ execution = result.execution;
122
+ workDir = result.worktreePath;
123
+ }
182
124
  }
183
125
  else {
184
126
  // Local mode - create execution without worktree
@@ -186,14 +128,40 @@ export class ExecutionService {
186
128
  execution = createExecution(this.db, {
187
129
  id: executionId,
188
130
  issue_id: issueId,
189
- agent_type: "claude-code",
131
+ agent_type: agentType,
190
132
  mode: mode,
191
- prompt: prompt,
133
+ prompt: prompt, // Store original (unexpanded) prompt
192
134
  config: JSON.stringify(config),
193
135
  target_branch: config.baseBranch || "main",
194
136
  branch_name: config.baseBranch || "main",
195
137
  });
196
138
  workDir = this.repoPath;
139
+ // Capture current commit as before_commit for local mode
140
+ try {
141
+ const beforeCommit = execSync("git rev-parse HEAD", {
142
+ cwd: this.repoPath,
143
+ encoding: "utf-8",
144
+ }).trim();
145
+ updateExecution(this.db, executionId, {
146
+ before_commit: beforeCommit,
147
+ });
148
+ // Reload execution to get updated before_commit
149
+ const updatedExecution = getExecution(this.db, executionId);
150
+ if (updatedExecution) {
151
+ execution = updatedExecution;
152
+ }
153
+ }
154
+ catch (error) {
155
+ console.warn("[ExecutionService] Failed to capture before_commit for local mode:", error instanceof Error ? error.message : String(error));
156
+ // Continue - this is supplementary data
157
+ }
158
+ }
159
+ // 3. Resolve prompt references for execution (done after storing original)
160
+ // Pass the issue ID so the issue content is automatically included even if not explicitly mentioned
161
+ const resolver = new PromptResolver(this.db);
162
+ const { resolvedPrompt, errors } = await resolver.resolve(prompt, new Set(), issueId);
163
+ if (errors.length > 0) {
164
+ console.warn(`[ExecutionService] Prompt resolution warnings:`, errors);
197
165
  }
198
166
  // Initialize empty logs for this execution
199
167
  try {
@@ -206,199 +174,76 @@ export class ExecutionService {
206
174
  });
207
175
  // Don't fail execution creation - logs are nice-to-have
208
176
  }
209
- // 3. Build WorkflowDefinition
210
- const workflow = {
211
- id: `workflow-${execution.id}`,
212
- steps: [
213
- {
214
- id: "execute-issue",
215
- taskType: "issue",
216
- prompt,
217
- taskConfig: {
218
- model: config.model || "claude-sonnet-4",
219
- timeout: config.timeout,
220
- captureFileChanges: config.captureFileChanges ?? true,
221
- captureToolCalls: config.captureToolCalls ?? true,
222
- },
223
- },
224
- ],
177
+ // 3. Start execution (use worker pool if available, otherwise fall back to in-process)
178
+ if (this.workerPool) {
179
+ // Worker pool handles all execution logic in isolated process
180
+ const dbPath = this.db.name;
181
+ await this.workerPool.startExecution(execution, this.repoPath, dbPath);
182
+ // Broadcast execution creation
183
+ broadcastExecutionUpdate(this.projectId, execution.id, "created", execution, execution.issue_id || undefined);
184
+ return execution;
185
+ }
186
+ // 4. In-process execution with executor wrapper (fallback when no worker pool)
187
+ const wrapper = createExecutorForAgent(agentType, { workDir: this.repoPath }, // Agent-specific config (minimal for now)
188
+ {
189
+ workDir: this.repoPath,
190
+ lifecycleService: this.lifecycleService,
191
+ logsStore: this.logsStore,
192
+ projectId: this.projectId,
193
+ db: this.db,
194
+ transportManager: this.transportManager,
195
+ });
196
+ // Build execution task (prompt already resolved above)
197
+ const task = {
198
+ id: execution.id,
199
+ type: "issue",
200
+ entityId: issueId,
201
+ prompt: resolvedPrompt,
202
+ workDir: workDir,
225
203
  config: {
226
- checkpointInterval: config.checkpointInterval ?? 1,
227
- continueOnStepFailure: config.continueOnStepFailure ?? false,
228
204
  timeout: config.timeout,
229
205
  },
230
206
  metadata: {
231
- workDir,
207
+ model: config.model || "claude-sonnet-4",
208
+ captureFileChanges: config.captureFileChanges ?? true,
209
+ captureToolCalls: config.captureToolCalls ?? true,
232
210
  issueId,
233
211
  executionId: execution.id,
234
212
  },
213
+ priority: 0,
214
+ dependencies: [],
215
+ createdAt: new Date(),
235
216
  };
236
- // 4. Create execution engine stack
237
- const processManager = new SimpleProcessManager();
238
- let engine = new SimpleExecutionEngine(processManager, {
239
- maxConcurrent: 1, // One task at a time for issue execution
240
- defaultProcessConfig: {
241
- executablePath: "claude",
242
- args: [
243
- "--print",
244
- "--output-format",
245
- "stream-json",
246
- "--verbose",
247
- "--dangerously-skip-permissions",
248
- ],
249
- },
217
+ // Execute with full lifecycle management (non-blocking)
218
+ wrapper.executeWithLifecycle(execution.id, task, workDir).catch((error) => {
219
+ console.error(`[ExecutionService] Execution ${execution.id} failed:`, error);
220
+ // Error is already handled by wrapper (status updated, broadcasts sent)
250
221
  });
251
- let executor = new ResilientExecutor(engine);
252
- // 5. Create AG-UI system (processor + adapter) if transport manager is available
253
- let agUiAdapter;
254
- if (this.transportManager) {
255
- const agUiSystem = createAgUiSystem(execution.id);
256
- agUiAdapter = agUiSystem.adapter;
257
- // Connect adapter to transport for SSE streaming
258
- this.transportManager.connectAdapter(agUiAdapter, execution.id);
259
- // Connect processor to execution engine for real-time output parsing
260
- // Buffer for incomplete lines (stream-json can split mid-line)
261
- let lineBuffer = "";
262
- engine = new SimpleExecutionEngine(processManager, {
263
- maxConcurrent: 1,
264
- defaultProcessConfig: {
265
- executablePath: "claude",
266
- args: [
267
- "--print",
268
- "--output-format",
269
- "stream-json",
270
- "--verbose",
271
- "--dangerously-skip-permissions",
272
- ],
273
- },
274
- // TODO: Factor out this logic for DRY principles.
275
- onOutput: (data, type) => {
276
- if (type === "stdout") {
277
- // Append new data to buffer
278
- lineBuffer += data.toString();
279
- // Process complete lines (ending with \n)
280
- let newlineIndex;
281
- while ((newlineIndex = lineBuffer.indexOf("\n")) !== -1) {
282
- const line = lineBuffer.slice(0, newlineIndex);
283
- lineBuffer = lineBuffer.slice(newlineIndex + 1);
284
- if (line.trim()) {
285
- // 1. Persist raw log immediately (before processing)
286
- try {
287
- this.logsStore.appendRawLog(execution.id, line);
288
- }
289
- catch (err) {
290
- console.error("[ExecutionService] Failed to persist raw log (non-critical):", {
291
- executionId: execution.id,
292
- error: err instanceof Error ? err.message : String(err),
293
- });
294
- // Don't crash execution - logs are nice-to-have
295
- }
296
- // 2. Process through AG-UI pipeline for live clients
297
- agUiSystem.processor.processLine(line).catch((err) => {
298
- console.error("[ExecutionService] Error processing output line:", {
299
- error: err instanceof Error ? err.message : String(err),
300
- line: line.slice(0, 100), // Log first 100 chars for debugging
301
- });
302
- });
303
- }
304
- }
305
- }
306
- },
307
- });
308
- executor = new ResilientExecutor(engine);
309
- }
310
- // 6. Create LinearOrchestrator
311
- const orchestrator = new LinearOrchestrator(executor, undefined, // No storage/checkpointing for now
312
- agUiAdapter, this.lifecycleService);
313
- // 7. Register event handlers to update execution status in database
314
- orchestrator.onWorkflowStart(() => {
315
- try {
316
- updateExecution(this.db, execution.id, {
317
- status: "running",
318
- });
319
- }
320
- catch (error) {
321
- console.error("[ExecutionService] Failed to update execution status to running", {
322
- executionId: execution.id,
323
- error: error instanceof Error ? error.message : String(error),
324
- });
325
- }
326
- });
327
- orchestrator.onWorkflowComplete(() => {
328
- console.log("[ExecutionService] Workflow completed successfully", {
329
- executionId: execution.id,
330
- });
331
- try {
332
- updateExecution(this.db, execution.id, {
333
- status: "completed",
334
- completed_at: new Date().toISOString(),
335
- });
336
- }
337
- catch (error) {
338
- console.error("[ExecutionService] Failed to update execution status to completed", {
339
- executionId: execution.id,
340
- error: error instanceof Error ? error.message : String(error),
341
- note: "Execution may have been deleted (e.g., due to CASCADE DELETE from issue deletion)",
342
- });
343
- }
344
- // Remove orchestrator from active map
345
- this.activeOrchestrators.delete(execution.id);
346
- });
347
- orchestrator.onWorkflowFailed((_executionId, error) => {
348
- console.error("[ExecutionService] Workflow failed", {
349
- executionId: execution.id,
350
- error: error.message,
351
- stack: error.stack,
352
- });
353
- try {
354
- updateExecution(this.db, execution.id, {
355
- status: "failed",
356
- completed_at: new Date().toISOString(),
357
- error_message: error.message,
358
- });
359
- }
360
- catch (updateError) {
361
- console.error("[ExecutionService] Failed to update execution status to failed", {
362
- executionId: execution.id,
363
- error: updateError instanceof Error
364
- ? updateError.message
365
- : String(updateError),
366
- note: "Execution may have been deleted (e.g., due to CASCADE DELETE from issue deletion)",
367
- });
368
- }
369
- // Remove orchestrator from active map
370
- this.activeOrchestrators.delete(execution.id);
371
- });
372
- // 8. Start workflow execution (non-blocking)
373
- orchestrator.startWorkflow(workflow, workDir, {
374
- checkpointInterval: config.checkpointInterval,
375
- executionId: execution.id,
376
- });
377
- // 9. Store orchestrator for later cancellation
378
- this.activeOrchestrators.set(execution.id, orchestrator);
222
+ // Broadcast execution creation
223
+ broadcastExecutionUpdate(this.projectId, execution.id, "created", execution, execution.issue_id || undefined);
379
224
  return execution;
380
225
  }
381
226
  /**
382
- * Create follow-up execution - reuse worktree from previous execution
227
+ * Create follow-up execution
383
228
  *
384
- * Creates a new execution that reuses the worktree from a previous execution,
385
- * appending feedback or additional context to the prompt.
229
+ * For worktree-based executions: reuses the worktree and resumes the session.
230
+ * For local/non-worktree executions: creates a new execution with feedback context.
386
231
  *
387
232
  * @param executionId - ID of previous execution to follow up on
388
233
  * @param feedback - Additional feedback/context to append to prompt
234
+ * @param options - Optional configuration
235
+ * @param options.includeOriginalPrompt - Whether to prepend the original issue content (default: false, assumes session resumption with full history)
389
236
  * @returns Created follow-up execution record
390
237
  */
391
- async createFollowUp(executionId, feedback) {
238
+ async createFollowUp(executionId, feedback, options) {
392
239
  // 1. Get previous execution
393
240
  const prevExecution = getExecution(this.db, executionId);
394
241
  if (!prevExecution) {
395
242
  throw new Error(`Execution ${executionId} not found`);
396
243
  }
397
- if (!prevExecution.worktree_path) {
398
- throw new Error(`Cannot create follow-up: execution ${executionId} has no worktree`);
399
- }
400
- // Check if worktree still exists on filesystem, recreate if needed
401
- if (this.lifecycleService) {
244
+ const hasWorktree = !!prevExecution.worktree_path;
245
+ // For worktree executions, check if worktree still exists on filesystem, recreate if needed
246
+ if (hasWorktree && this.lifecycleService) {
402
247
  const fs = await import("fs");
403
248
  const worktreeExists = fs.existsSync(prevExecution.worktree_path);
404
249
  if (!worktreeExists) {
@@ -414,30 +259,42 @@ export class ExecutionService {
414
259
  });
415
260
  }
416
261
  }
417
- // 2. Validate that previous execution has an issue_id
418
- if (!prevExecution.issue_id) {
419
- throw new Error("Previous execution must have an issue_id for follow-up");
420
- }
421
- // 3. Prepare execution to get rendered prompt
422
- const prepareResult = await this.prepareExecution(prevExecution.issue_id);
423
- // 4. Append feedback to prompt
424
- const followUpPrompt = `${prepareResult.renderedPrompt}
425
-
426
- ## Follow-up Feedback
427
- ${feedback}
262
+ // TODO: Make it so follow-ups don't require an issue id.
263
+ // 2. Build follow-up prompt (default: just feedback, assumes session resumption)
264
+ let followUpPrompt = feedback;
265
+ if (options?.includeOriginalPrompt) {
266
+ // Optional: include original issue content if explicitly requested
267
+ if (!prevExecution.issue_id) {
268
+ throw new Error("Previous execution must have an issue_id to include original prompt");
269
+ }
270
+ // Get issue content directly from database
271
+ const issue = this.db
272
+ .prepare("SELECT content FROM issues WHERE id = ?")
273
+ .get(prevExecution.issue_id);
274
+ if (!issue) {
275
+ throw new Error(`Issue ${prevExecution.issue_id} not found`);
276
+ }
277
+ followUpPrompt = `${issue.content}
428
278
 
429
- Please continue working on this issue, taking into account the feedback above.`;
430
- // 5. Create new execution record that references previous execution
279
+ ${feedback}`;
280
+ }
281
+ // 3. Create new execution record that references previous execution
282
+ // Default to 'claude-code' if agent_type is null (for backwards compatibility)
283
+ const agentType = (prevExecution.agent_type || "claude-code");
284
+ // Determine working directory: worktree path if available, otherwise repo path (local mode)
285
+ const workDir = hasWorktree ? prevExecution.worktree_path : this.repoPath;
431
286
  const newExecutionId = randomUUID();
432
287
  const newExecution = createExecution(this.db, {
433
288
  id: newExecutionId,
434
289
  issue_id: prevExecution.issue_id,
435
- agent_type: "claude-code",
290
+ agent_type: agentType, // Use same agent as previous execution
291
+ mode: prevExecution.mode || (hasWorktree ? "worktree" : "local"), // Inherit mode from parent
436
292
  target_branch: prevExecution.target_branch,
437
293
  branch_name: prevExecution.branch_name,
438
- // TODO: Handle case where worktree has been deleted.
439
- worktree_path: prevExecution.worktree_path, // Reuse same worktree
294
+ worktree_path: prevExecution.worktree_path || undefined, // Reuse same worktree (undefined for local)
440
295
  config: prevExecution.config || undefined, // Preserve config (including cleanupMode) from previous execution
296
+ parent_execution_id: executionId, // Link to parent execution for follow-up chain
297
+ prompt: followUpPrompt, // Store original (unexpanded) follow-up prompt
441
298
  });
442
299
  // Initialize empty logs for this execution
443
300
  try {
@@ -450,161 +307,81 @@ Please continue working on this issue, taking into account the feedback above.`;
450
307
  });
451
308
  // Don't fail execution creation - logs are nice-to-have
452
309
  }
453
- // 5. Build WorkflowDefinition
454
- const workflow = {
455
- id: `workflow-${newExecution.id}`,
456
- steps: [
457
- {
458
- id: "execute-followup",
459
- taskType: "issue",
460
- prompt: followUpPrompt,
461
- taskConfig: {
462
- model: "claude-sonnet-4",
463
- captureFileChanges: true,
464
- captureToolCalls: true,
465
- },
466
- },
467
- ],
310
+ // Collect already-expanded entities from parent execution chain
311
+ const alreadyExpandedIds = await this.collectExpandedEntitiesFromChain(executionId);
312
+ // Resolve prompt references for execution (done after storing original)
313
+ // Skip entities that were already expanded in parent executions
314
+ const resolver = new PromptResolver(this.db);
315
+ const { resolvedPrompt, errors } = await resolver.resolve(followUpPrompt, alreadyExpandedIds);
316
+ if (errors.length > 0) {
317
+ console.warn(`[ExecutionService] Follow-up prompt resolution warnings:`, errors);
318
+ }
319
+ // 4. Use executor wrapper with session resumption
320
+ const wrapper = createExecutorForAgent(agentType, { workDir: this.repoPath }, {
321
+ workDir: this.repoPath,
322
+ lifecycleService: this.lifecycleService,
323
+ logsStore: this.logsStore,
324
+ projectId: this.projectId,
325
+ db: this.db,
326
+ transportManager: this.transportManager,
327
+ });
328
+ // Use previous execution's session_id (the actual Claude UUID) if available
329
+ // This enables proper session resumption with Claude Code's --resume-session flag
330
+ // If no session_id was captured, we can't resume - this would start a new session
331
+ const sessionId = prevExecution.session_id;
332
+ if (!sessionId) {
333
+ console.warn(`[ExecutionService] No session_id found for execution ${executionId}, follow-up will start a new session`);
334
+ }
335
+ // Parse config to get model and other settings
336
+ const parsedConfig = prevExecution.config
337
+ ? JSON.parse(prevExecution.config)
338
+ : {};
339
+ // Build execution task for follow-up (use resolved prompt for agent)
340
+ const task = {
341
+ id: newExecution.id,
342
+ type: "issue",
343
+ entityId: prevExecution.issue_id ?? undefined,
344
+ prompt: resolvedPrompt,
345
+ workDir: workDir,
468
346
  config: {
469
- checkpointInterval: 1,
470
- continueOnStepFailure: false,
347
+ timeout: parsedConfig.timeout,
471
348
  },
472
349
  metadata: {
473
- workDir: prevExecution.worktree_path,
474
- issueId: prevExecution.issue_id,
350
+ model: parsedConfig.model || "claude-sonnet-4",
351
+ captureFileChanges: parsedConfig.captureFileChanges ?? true,
352
+ captureToolCalls: parsedConfig.captureToolCalls ?? true,
353
+ issueId: prevExecution.issue_id ?? undefined,
475
354
  executionId: newExecution.id,
476
355
  followUpOf: executionId,
477
356
  },
357
+ priority: 0,
358
+ dependencies: [],
359
+ createdAt: new Date(),
478
360
  };
479
- // 6. Create execution engine stack
480
- const processManager = new SimpleProcessManager();
481
- let engine = new SimpleExecutionEngine(processManager, {
482
- maxConcurrent: 1,
483
- defaultProcessConfig: {
484
- executablePath: "claude",
485
- args: [
486
- "--print",
487
- "--output-format",
488
- "stream-json",
489
- "--verbose",
490
- "--dangerously-skip-permissions",
491
- ],
492
- },
493
- });
494
- let executor = new ResilientExecutor(engine);
495
- // 7. Create AG-UI system (processor + adapter) if transport manager is available
496
- let agUiAdapter;
497
- if (this.transportManager) {
498
- const agUiSystem = createAgUiSystem(newExecution.id);
499
- agUiAdapter = agUiSystem.adapter;
500
- this.transportManager.connectAdapter(agUiAdapter, newExecution.id);
501
- // Connect processor to execution engine for real-time output parsing
502
- // Buffer for incomplete lines (stream-json can split mid-line)
503
- let lineBuffer = "";
504
- engine = new SimpleExecutionEngine(processManager, {
505
- maxConcurrent: 1,
506
- defaultProcessConfig: {
507
- executablePath: "claude",
508
- args: [
509
- "--print",
510
- "--output-format",
511
- "stream-json",
512
- "--verbose",
513
- "--dangerously-skip-permissions",
514
- ],
515
- },
516
- // TODO: Factor out this logic for DRY principles.
517
- onOutput: (data, type) => {
518
- if (type === "stdout") {
519
- // Append new data to buffer
520
- lineBuffer += data.toString();
521
- // Process complete lines (ending with \n)
522
- let newlineIndex;
523
- while ((newlineIndex = lineBuffer.indexOf("\n")) !== -1) {
524
- const line = lineBuffer.slice(0, newlineIndex);
525
- lineBuffer = lineBuffer.slice(newlineIndex + 1);
526
- if (line.trim()) {
527
- // 1. Persist raw log immediately (before processing)
528
- try {
529
- this.logsStore.appendRawLog(newExecution.id, line);
530
- }
531
- catch (err) {
532
- console.error("[ExecutionService] Failed to persist raw log (non-critical):", {
533
- executionId: newExecution.id,
534
- error: err instanceof Error ? err.message : String(err),
535
- });
536
- // Don't crash execution - logs are nice-to-have
537
- }
538
- // 2. Process through AG-UI pipeline for live clients
539
- agUiSystem.processor.processLine(line).catch((err) => {
540
- console.error("[ExecutionService] Error processing output line:", {
541
- error: err instanceof Error ? err.message : String(err),
542
- line: line.slice(0, 100), // Log first 100 chars for debugging
543
- });
544
- });
545
- }
546
- }
547
- }
548
- },
361
+ // Execute follow-up (non-blocking)
362
+ // If we have a session ID, resume the session; otherwise start a new one
363
+ if (sessionId) {
364
+ wrapper
365
+ .resumeWithLifecycle(newExecution.id, sessionId, task, workDir)
366
+ .catch((error) => {
367
+ console.error(`[ExecutionService] Follow-up execution ${newExecution.id} failed:`, error);
368
+ // Error is already handled by wrapper (status updated, broadcasts sent)
549
369
  });
550
- executor = new ResilientExecutor(engine);
551
370
  }
552
- // 8. Create LinearOrchestrator
553
- const orchestrator = new LinearOrchestrator(executor, undefined, agUiAdapter, this.lifecycleService);
554
- // 9. Register event handlers
555
- orchestrator.onWorkflowStart(() => {
556
- try {
557
- updateExecution(this.db, newExecution.id, {
558
- status: "running",
559
- });
560
- }
561
- catch (error) {
562
- console.error("[ExecutionService] Failed to update follow-up execution status to running", {
563
- executionId: newExecution.id,
564
- error: error instanceof Error ? error.message : String(error),
565
- });
566
- }
567
- });
568
- orchestrator.onWorkflowComplete(() => {
569
- try {
570
- updateExecution(this.db, newExecution.id, {
571
- status: "completed",
572
- completed_at: new Date().toISOString(),
573
- });
574
- }
575
- catch (error) {
576
- console.error("[ExecutionService] Failed to update follow-up execution status to completed", {
577
- executionId: newExecution.id,
578
- error: error instanceof Error ? error.message : String(error),
579
- });
580
- }
581
- this.activeOrchestrators.delete(newExecution.id);
582
- });
583
- orchestrator.onWorkflowFailed((_execId, error) => {
584
- try {
585
- updateExecution(this.db, newExecution.id, {
586
- status: "failed",
587
- completed_at: new Date().toISOString(),
588
- error_message: error.message,
589
- });
590
- }
591
- catch (updateError) {
592
- console.error("[ExecutionService] Failed to update follow-up execution status to failed", {
593
- executionId: newExecution.id,
594
- error: updateError instanceof Error
595
- ? updateError.message
596
- : String(updateError),
597
- });
598
- }
599
- this.activeOrchestrators.delete(newExecution.id);
600
- });
601
- // 10. Start workflow execution (non-blocking)
602
- orchestrator.startWorkflow(workflow, prevExecution.worktree_path, {
603
- checkpointInterval: 1,
604
- executionId: newExecution.id,
605
- });
606
- // 11. Store orchestrator for later cancellation
607
- this.activeOrchestrators.set(newExecution.id, orchestrator);
371
+ else {
372
+ // No session to resume, start a new execution with the follow-up prompt
373
+ wrapper
374
+ .executeWithLifecycle(newExecution.id, task, workDir)
375
+ .catch((error) => {
376
+ console.error(`[ExecutionService] Follow-up execution ${newExecution.id} failed:`, error);
377
+ });
378
+ }
379
+ // Broadcast execution creation
380
+ broadcastExecutionUpdate(this.projectId, newExecution.id, "created", newExecution, newExecution.issue_id || undefined);
381
+ // Also broadcast to parent execution channel
382
+ if (newExecution.parent_execution_id) {
383
+ broadcastExecutionUpdate(this.projectId, newExecution.parent_execution_id, "updated", newExecution, newExecution.issue_id || undefined);
384
+ }
608
385
  return newExecution;
609
386
  }
610
387
  /**
@@ -623,19 +400,26 @@ Please continue working on this issue, taking into account the feedback above.`;
623
400
  if (execution.status !== "running") {
624
401
  throw new Error(`Cannot cancel execution in ${execution.status} state`);
625
402
  }
626
- // Get orchestrator from active map
627
- const orchestrator = this.activeOrchestrators.get(executionId);
628
- if (orchestrator) {
629
- // Cancel via orchestrator
630
- await orchestrator.cancelWorkflow(executionId);
631
- // Remove from active map
632
- this.activeOrchestrators.delete(executionId);
403
+ // Use worker pool cancellation if available
404
+ if (this.workerPool && this.workerPool.hasWorker(executionId)) {
405
+ await this.workerPool.cancelExecution(executionId);
406
+ return; // Worker pool handles DB updates and broadcasts
633
407
  }
634
- // Update status in database (orchestrator.cancelWorkflow doesn't emit events for DB update)
408
+ // For in-process executions using AgentExecutorWrapper:
409
+ // The wrapper manages its own lifecycle and cancellation.
410
+ // We update the database status, which the wrapper may check,
411
+ // or we rely on process termination to stop execution.
412
+ // TODO: Add cancellation registry in AgentExecutorWrapper for direct process control
413
+ // Update status in database
635
414
  updateExecution(this.db, executionId, {
636
415
  status: "stopped",
637
416
  completed_at: new Date().toISOString(),
638
417
  });
418
+ // Broadcast status change
419
+ const updated = getExecution(this.db, executionId);
420
+ if (updated) {
421
+ broadcastExecutionUpdate(this.projectId, executionId, "status_changed", updated, updated.issue_id || undefined);
422
+ }
639
423
  }
640
424
  /**
641
425
  * Clean up execution resources
@@ -670,9 +454,10 @@ Please continue working on this issue, taking into account the feedback above.`;
670
454
  * when they're configured for manual cleanup.
671
455
  *
672
456
  * @param executionId - ID of execution whose worktree to delete
457
+ * @param deleteBranch - Whether to also delete the execution's branch (default: false)
673
458
  * @throws Error if execution not found, has no worktree, or worktree doesn't exist
674
459
  */
675
- async deleteWorktree(executionId) {
460
+ async deleteWorktree(executionId, deleteBranch = false) {
676
461
  const execution = getExecution(this.db, executionId);
677
462
  if (!execution) {
678
463
  throw new Error(`Execution ${executionId} not found`);
@@ -691,6 +476,146 @@ Please continue working on this issue, taking into account the feedback above.`;
691
476
  const worktreeManager = this.lifecycleService.worktreeManager;
692
477
  // Clean up the worktree
693
478
  await worktreeManager.cleanupWorktree(execution.worktree_path, this.repoPath);
479
+ // Delete branch if requested and it was created by this execution
480
+ if (deleteBranch && execution.branch_name) {
481
+ try {
482
+ // A branch was created for this execution if:
483
+ // - branch_name is DIFFERENT from target_branch (autoCreateBranches was true)
484
+ // - This means a new worktree-specific branch was created
485
+ const wasCreatedByExecution = execution.branch_name !== execution.target_branch &&
486
+ execution.branch_name !== "(detached)";
487
+ if (wasCreatedByExecution) {
488
+ await worktreeManager.git.deleteBranch(this.repoPath, execution.branch_name, true // Force deletion
489
+ );
490
+ console.log(`[ExecutionService] Deleted execution-created branch: ${execution.branch_name}`);
491
+ }
492
+ else {
493
+ console.log(`[ExecutionService] Skipping branch deletion - branch ${execution.branch_name} is the target branch (not created by execution)`);
494
+ }
495
+ }
496
+ catch (err) {
497
+ console.warn(`Failed to delete branch ${execution.branch_name} during worktree deletion:`, err);
498
+ // Continue even if branch deletion fails
499
+ }
500
+ }
501
+ }
502
+ /**
503
+ * Delete an execution and its entire chain
504
+ *
505
+ * Deletes the execution and all its follow-ups (descendants).
506
+ * Optionally deletes the worktree and/or branch.
507
+ *
508
+ * @param executionId - ID of execution to delete (can be root or any execution in chain)
509
+ * @param deleteBranch - Whether to also delete the execution's branch (default: false)
510
+ * @param deleteWorktree - Whether to also delete the execution's worktree (default: false)
511
+ * @throws Error if execution not found
512
+ */
513
+ async deleteExecution(executionId, deleteBranch = false, deleteWorktree = false) {
514
+ const execution = getExecution(this.db, executionId);
515
+ if (!execution) {
516
+ throw new Error(`Execution ${executionId} not found`);
517
+ }
518
+ // Find the root execution by traversing up parent_execution_id
519
+ let rootId = executionId;
520
+ let current = execution;
521
+ while (current.parent_execution_id) {
522
+ rootId = current.parent_execution_id;
523
+ const parent = getExecution(this.db, rootId);
524
+ if (!parent)
525
+ break;
526
+ current = parent;
527
+ }
528
+ // Get all executions in the chain (root + all descendants)
529
+ const chain = this.db
530
+ .prepare(`
531
+ WITH RECURSIVE execution_chain AS (
532
+ -- Base case: the root execution
533
+ SELECT * FROM executions WHERE id = ?
534
+ UNION ALL
535
+ -- Recursive case: children of executions in the chain
536
+ SELECT e.* FROM executions e
537
+ INNER JOIN execution_chain ec ON e.parent_execution_id = ec.id
538
+ )
539
+ SELECT * FROM execution_chain
540
+ `)
541
+ .all(rootId);
542
+ // Cancel any running executions in the chain
543
+ for (const exec of chain) {
544
+ if (exec.status === "running" || exec.status === "pending") {
545
+ try {
546
+ await this.cancelExecution(exec.id);
547
+ }
548
+ catch (err) {
549
+ console.warn(`Failed to cancel execution ${exec.id} during deletion:`, err);
550
+ // Continue with deletion even if cancel fails
551
+ }
552
+ }
553
+ }
554
+ // Delete worktree if requested and it exists (only for root execution)
555
+ const rootExecution = chain.find((e) => e.id === rootId);
556
+ if (deleteWorktree && rootExecution?.worktree_path) {
557
+ try {
558
+ const fs = await import("fs");
559
+ if (fs.existsSync(rootExecution.worktree_path)) {
560
+ await this.deleteWorktree(rootId);
561
+ }
562
+ }
563
+ catch (err) {
564
+ console.warn(`Failed to delete worktree during execution deletion:`, err);
565
+ // Continue with deletion even if worktree cleanup fails
566
+ }
567
+ }
568
+ // Delete branch if requested and it exists
569
+ // IMPORTANT: Only delete branches that were created specifically for this execution
570
+ if (deleteBranch && rootExecution?.branch_name) {
571
+ try {
572
+ // A branch was created for this execution if:
573
+ // - branch_name is DIFFERENT from target_branch (autoCreateBranches was true)
574
+ // - This means a new worktree-specific branch was created
575
+ //
576
+ // A branch was NOT created (reusing existing) if:
577
+ // - branch_name === target_branch (autoCreateBranches was false)
578
+ // - This means the worktree reused the target branch directly
579
+ const wasCreatedByExecution = rootExecution.branch_name !== rootExecution.target_branch &&
580
+ rootExecution.branch_name !== "(detached)";
581
+ if (wasCreatedByExecution) {
582
+ // Get worktree manager from lifecycle service to access git operations
583
+ const worktreeManager = this.lifecycleService
584
+ .worktreeManager;
585
+ await worktreeManager.git.deleteBranch(this.repoPath, rootExecution.branch_name, true // Force deletion
586
+ );
587
+ console.log(`[ExecutionService] Deleted execution-created branch: ${rootExecution.branch_name}`);
588
+ }
589
+ else {
590
+ console.log(`[ExecutionService] Skipping branch deletion - branch ${rootExecution.branch_name} is the target branch (not created by execution)`);
591
+ }
592
+ }
593
+ catch (err) {
594
+ console.warn(`Failed to delete branch ${rootExecution.branch_name} during execution deletion:`, err);
595
+ // Continue with deletion even if branch deletion fails
596
+ }
597
+ }
598
+ // Delete execution logs for all executions in the chain
599
+ for (const exec of chain) {
600
+ try {
601
+ this.logsStore.deleteLogs(exec.id);
602
+ }
603
+ catch (err) {
604
+ console.warn(`Failed to delete logs for execution ${exec.id}:`, err);
605
+ // Continue with deletion even if log cleanup fails
606
+ }
607
+ }
608
+ // Delete all executions in the chain from database
609
+ // Delete in reverse order (children first) to avoid foreign key issues
610
+ const chainIds = chain.map((e) => e.id);
611
+ const placeholders = chainIds.map(() => "?").join(",");
612
+ this.db
613
+ .prepare(`DELETE FROM executions WHERE id IN (${placeholders})`)
614
+ .run(...chainIds);
615
+ // Broadcast deletion event for each execution
616
+ for (const exec of chain) {
617
+ broadcastExecutionUpdate(this.projectId, exec.id, "deleted", { executionId: exec.id }, exec.issue_id || undefined);
618
+ }
694
619
  }
695
620
  /**
696
621
  * Shutdown execution service - cancel all active executions
@@ -699,21 +624,14 @@ Please continue working on this issue, taking into account the feedback above.`;
699
624
  * all running executions before the server exits.
700
625
  */
701
626
  async shutdown() {
702
- const cancelPromises = [];
703
- // Cancel all active orchestrators
704
- for (const [executionId, orchestrator,] of this.activeOrchestrators.entries()) {
705
- cancelPromises.push(orchestrator.cancelWorkflow(executionId).catch((error) => {
706
- console.error("[ExecutionService] Error canceling execution", {
707
- executionId,
708
- error: error.message,
709
- });
710
- }));
627
+ // Shutdown worker pool if available
628
+ if (this.workerPool) {
629
+ await this.workerPool.shutdown();
711
630
  }
712
- // Wait for all cancellations to complete (with timeout)
713
- await Promise.race([
714
- Promise.all(cancelPromises),
715
- new Promise((resolve) => setTimeout(resolve, 5000)), // 5 second timeout
716
- ]);
631
+ // For in-process executions using AgentExecutorWrapper:
632
+ // The wrapper manages its own lifecycle. Processes will be terminated
633
+ // when the Node.js process exits.
634
+ // TODO: Add active execution tracking to AgentExecutorWrapper for graceful shutdown
717
635
  }
718
636
  /**
719
637
  * List all executions for an issue
@@ -743,5 +661,132 @@ Please continue working on this issue, taking into account the feedback above.`;
743
661
  getExecution(executionId) {
744
662
  return getExecution(this.db, executionId);
745
663
  }
664
+ /**
665
+ * List all executions with filtering and pagination
666
+ *
667
+ * Returns executions across all issues with support for filtering
668
+ * by status, issueId, and pagination.
669
+ *
670
+ * @param options - Filtering and pagination options
671
+ * @param options.limit - Maximum number of executions to return (default: 50)
672
+ * @param options.offset - Number of executions to skip (default: 0)
673
+ * @param options.status - Filter by execution status (single or array)
674
+ * @param options.issueId - Filter by issue ID
675
+ * @param options.sortBy - Field to sort by (default: 'created_at')
676
+ * @param options.order - Sort order (default: 'desc')
677
+ * @param options.since - Only return executions created after this ISO date
678
+ * @param options.includeRunning - When used with 'since', also include running executions regardless of age
679
+ * @returns Object containing executions array, total count, and hasMore flag
680
+ */
681
+ listAll(options = {}) {
682
+ const limit = options.limit ?? 50;
683
+ const offset = options.offset ?? 0;
684
+ const sortBy = options.sortBy ?? "created_at";
685
+ const order = options.order ?? "desc";
686
+ // Validate inputs
687
+ if (limit < 0 || offset < 0) {
688
+ throw new Error("Limit and offset must be non-negative");
689
+ }
690
+ // Build WHERE clause dynamically
691
+ const whereClauses = [];
692
+ const params = [];
693
+ // Filter by status (single or array)
694
+ if (options.status) {
695
+ const statuses = Array.isArray(options.status)
696
+ ? options.status
697
+ : [options.status];
698
+ const placeholders = statuses.map(() => "?").join(",");
699
+ whereClauses.push(`status IN (${placeholders})`);
700
+ params.push(...statuses);
701
+ }
702
+ // Filter by issueId
703
+ if (options.issueId) {
704
+ whereClauses.push("issue_id = ?");
705
+ params.push(options.issueId);
706
+ }
707
+ // Filter by since date (with optional includeRunning)
708
+ if (options.since) {
709
+ if (options.includeRunning) {
710
+ // Include executions created after 'since' OR that are currently running
711
+ whereClauses.push("(created_at >= ? OR status = 'running')");
712
+ params.push(options.since);
713
+ }
714
+ else {
715
+ // Only include executions created after 'since'
716
+ whereClauses.push("created_at >= ?");
717
+ params.push(options.since);
718
+ }
719
+ }
720
+ // Build WHERE clause
721
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
722
+ // Get total count
723
+ const countQuery = `SELECT COUNT(*) as count FROM executions ${whereClause}`;
724
+ const countResult = this.db.prepare(countQuery).get(...params);
725
+ const total = countResult.count;
726
+ // Get executions with pagination
727
+ const query = `
728
+ SELECT * FROM executions
729
+ ${whereClause}
730
+ ORDER BY ${sortBy} ${order.toUpperCase()}
731
+ LIMIT ? OFFSET ?
732
+ `;
733
+ const executions = this.db
734
+ .prepare(query)
735
+ .all(...params, limit, offset);
736
+ // Calculate hasMore
737
+ const hasMore = offset + executions.length < total;
738
+ return {
739
+ executions,
740
+ total,
741
+ hasMore,
742
+ };
743
+ }
744
+ /**
745
+ * Check if there are any active executions
746
+ *
747
+ * @returns true if there are active worker pool executions
748
+ */
749
+ hasActiveExecutions() {
750
+ // Check worker pool for active executions
751
+ if (this.workerPool) {
752
+ return this.workerPool.getActiveWorkerCount() > 0;
753
+ }
754
+ // For in-process executions, we don't track them anymore
755
+ // Query the database for running executions as a fallback
756
+ const runningExecutions = this.db
757
+ .prepare("SELECT COUNT(*) as count FROM executions WHERE status = 'running'")
758
+ .get();
759
+ return runningExecutions.count > 0;
760
+ }
761
+ /**
762
+ * Collect entity IDs that were already expanded in parent executions
763
+ *
764
+ * Walks the execution chain backwards and resolves each prompt to extract
765
+ * which entities were referenced (and thus expanded) in previous executions.
766
+ * This prevents redundant expansion of the same entities in follow-ups.
767
+ *
768
+ * @param executionId - ID of the current execution
769
+ * @returns Set of entity IDs that were already expanded
770
+ */
771
+ async collectExpandedEntitiesFromChain(executionId) {
772
+ const expandedIds = new Set();
773
+ const resolver = new PromptResolver(this.db);
774
+ // Walk backwards through the execution chain
775
+ let currentExecId = executionId;
776
+ while (currentExecId) {
777
+ const execution = getExecution(this.db, currentExecId);
778
+ if (!execution || !execution.prompt)
779
+ break;
780
+ // Resolve the prompt to extract what entities were referenced
781
+ // Pass empty set so we expand everything in this pass (just to collect IDs)
782
+ // Pass the execution's issue_id as implicit to track if it was auto-included
783
+ const { expandedEntityIds } = await resolver.resolve(execution.prompt, new Set(), execution.issue_id || undefined);
784
+ // Add all expanded entity IDs from this execution
785
+ expandedEntityIds.forEach((id) => expandedIds.add(id));
786
+ // Move to parent execution
787
+ currentExecId = execution.parent_execution_id || null;
788
+ }
789
+ return expandedIds;
790
+ }
746
791
  }
747
792
  //# sourceMappingURL=execution-service.js.map