mcoda 0.1.2 → 0.1.4

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 (461) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +9 -300
  3. package/dist/bin/McodaEntrypoint.d.ts +5 -0
  4. package/dist/bin/McodaEntrypoint.d.ts.map +1 -0
  5. package/dist/bin/McodaEntrypoint.js +175 -0
  6. package/dist/commands/agents/AgentsCommands.d.ts +4 -0
  7. package/dist/commands/agents/AgentsCommands.d.ts.map +1 -0
  8. package/dist/commands/agents/AgentsCommands.js +376 -0
  9. package/dist/commands/agents/GatewayAgentCommand.d.ts +4 -0
  10. package/dist/commands/agents/GatewayAgentCommand.d.ts.map +1 -0
  11. package/dist/commands/agents/GatewayAgentCommand.js +583 -0
  12. package/dist/commands/agents/TestAgentCommand.d.ts +4 -0
  13. package/dist/commands/agents/TestAgentCommand.d.ts.map +1 -0
  14. package/dist/commands/agents/TestAgentCommand.js +57 -0
  15. package/dist/commands/backlog/BacklogCommands.d.ts +17 -0
  16. package/dist/commands/backlog/BacklogCommands.d.ts.map +1 -0
  17. package/dist/commands/backlog/BacklogCommands.js +260 -0
  18. package/dist/commands/backlog/OrderTasksCommand.d.ts +16 -0
  19. package/dist/commands/backlog/OrderTasksCommand.d.ts.map +1 -0
  20. package/dist/commands/backlog/OrderTasksCommand.js +211 -0
  21. package/dist/commands/backlog/TaskShowCommands.d.ts +16 -0
  22. package/dist/commands/backlog/TaskShowCommands.d.ts.map +1 -0
  23. package/dist/commands/backlog/TaskShowCommands.js +275 -0
  24. package/dist/commands/docs/DocsCommands.d.ts +37 -0
  25. package/dist/commands/docs/DocsCommands.d.ts.map +1 -0
  26. package/dist/commands/docs/DocsCommands.js +381 -0
  27. package/dist/commands/estimate/EstimateCommands.d.ts +24 -0
  28. package/dist/commands/estimate/EstimateCommands.d.ts.map +1 -0
  29. package/dist/commands/estimate/EstimateCommands.js +259 -0
  30. package/dist/commands/jobs/JobsCommands.d.ts +24 -0
  31. package/dist/commands/jobs/JobsCommands.d.ts.map +1 -0
  32. package/dist/commands/jobs/JobsCommands.js +535 -0
  33. package/dist/commands/openapi/OpenapiCommands.d.ts +14 -0
  34. package/dist/commands/openapi/OpenapiCommands.d.ts.map +1 -0
  35. package/dist/commands/openapi/OpenapiCommands.js +157 -0
  36. package/dist/commands/planning/CreateTasksCommand.d.ts +17 -0
  37. package/dist/commands/planning/CreateTasksCommand.d.ts.map +1 -0
  38. package/dist/commands/planning/CreateTasksCommand.js +134 -0
  39. package/dist/commands/planning/MigrateTasksCommand.d.ts +15 -0
  40. package/dist/commands/planning/MigrateTasksCommand.d.ts.map +1 -0
  41. package/dist/commands/planning/MigrateTasksCommand.js +95 -0
  42. package/dist/commands/planning/PlanningCommands.d.ts +3 -0
  43. package/dist/commands/planning/PlanningCommands.d.ts.map +1 -0
  44. package/dist/commands/planning/PlanningCommands.js +2 -0
  45. package/dist/commands/planning/QaTasksCommand.d.ts +30 -0
  46. package/dist/commands/planning/QaTasksCommand.d.ts.map +1 -0
  47. package/dist/commands/planning/QaTasksCommand.js +293 -0
  48. package/dist/commands/planning/RefineTasksCommand.d.ts +30 -0
  49. package/dist/commands/planning/RefineTasksCommand.d.ts.map +1 -0
  50. package/dist/commands/planning/RefineTasksCommand.js +365 -0
  51. package/dist/commands/review/CodeReviewCommand.d.ts +21 -0
  52. package/dist/commands/review/CodeReviewCommand.d.ts.map +1 -0
  53. package/dist/commands/review/CodeReviewCommand.js +236 -0
  54. package/dist/commands/routing/RoutingCommands.d.ts +7 -0
  55. package/dist/commands/routing/RoutingCommands.d.ts.map +1 -0
  56. package/dist/commands/routing/RoutingCommands.js +484 -0
  57. package/dist/commands/telemetry/TelemetryCommands.d.ts +26 -0
  58. package/dist/commands/telemetry/TelemetryCommands.d.ts.map +1 -0
  59. package/dist/commands/telemetry/TelemetryCommands.js +313 -0
  60. package/dist/commands/update/UpdateCommands.d.ts +4 -0
  61. package/dist/commands/update/UpdateCommands.d.ts.map +1 -0
  62. package/dist/commands/update/UpdateCommands.js +280 -0
  63. package/dist/commands/work/WorkOnTasksCommand.d.ts +21 -0
  64. package/dist/commands/work/WorkOnTasksCommand.d.ts.map +1 -0
  65. package/dist/commands/work/WorkOnTasksCommand.js +238 -0
  66. package/dist/commands/workspace/SetWorkspaceCommand.d.ts +9 -0
  67. package/dist/commands/workspace/SetWorkspaceCommand.d.ts.map +1 -0
  68. package/dist/commands/workspace/SetWorkspaceCommand.js +128 -0
  69. package/dist/index.d.ts +19 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/package.json +31 -16
  72. package/.editorconfig +0 -9
  73. package/.eslintrc.cjs +0 -12
  74. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
  75. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  76. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  77. package/.github/workflows/ci.yml +0 -37
  78. package/.github/workflows/nightly.yml +0 -38
  79. package/.github/workflows/release-dry-run.yml +0 -40
  80. package/.github/workflows/release-please.yml +0 -22
  81. package/.github/workflows/release.yml +0 -139
  82. package/.prettierrc +0 -5
  83. package/.release-please-manifest.json +0 -8
  84. package/CLA.md +0 -42
  85. package/CONTRIBUTING.md +0 -38
  86. package/docs/oss_publishing_plan.md +0 -41
  87. package/docs/pdr/.gitkeep +0 -0
  88. package/docs/quality_gates.md +0 -32
  89. package/docs/rfp/.gitkeep +0 -0
  90. package/docs/sds/sds.md +0 -11963
  91. package/docs/usage.md +0 -72
  92. package/openapi/gen-openapi.ts +0 -1
  93. package/openapi/generated/clients/.gitkeep +0 -0
  94. package/openapi/generated/types/.gitkeep +0 -0
  95. package/openapi/generated/types/index.ts +0 -118
  96. package/openapi/mcoda.yaml +0 -2063
  97. package/pack-mcoda.sh +0 -88
  98. package/packages/agents/CHANGELOG.md +0 -7
  99. package/packages/agents/LICENSE +0 -21
  100. package/packages/agents/README.md +0 -9
  101. package/packages/agents/package.json +0 -41
  102. package/packages/agents/src/AgentService/.gitkeep +0 -0
  103. package/packages/agents/src/AgentService/AgentService.d.ts +0 -21
  104. package/packages/agents/src/AgentService/AgentService.d.ts.map +0 -1
  105. package/packages/agents/src/AgentService/AgentService.js +0 -141
  106. package/packages/agents/src/AgentService/AgentService.ts +0 -308
  107. package/packages/agents/src/__tests__/AgentService.test.ts +0 -284
  108. package/packages/agents/src/adapters/AdapterTypes.d.ts +0 -29
  109. package/packages/agents/src/adapters/AdapterTypes.d.ts.map +0 -1
  110. package/packages/agents/src/adapters/AdapterTypes.js +0 -1
  111. package/packages/agents/src/adapters/AdapterTypes.ts +0 -32
  112. package/packages/agents/src/adapters/codex/.gitkeep +0 -0
  113. package/packages/agents/src/adapters/codex/CodexAdapter.d.ts +0 -11
  114. package/packages/agents/src/adapters/codex/CodexAdapter.d.ts.map +0 -1
  115. package/packages/agents/src/adapters/codex/CodexAdapter.js +0 -43
  116. package/packages/agents/src/adapters/codex/CodexAdapter.ts +0 -63
  117. package/packages/agents/src/adapters/codex/CodexCliRunner.ts +0 -154
  118. package/packages/agents/src/adapters/gemini/.gitkeep +0 -0
  119. package/packages/agents/src/adapters/gemini/GeminiAdapter.d.ts +0 -11
  120. package/packages/agents/src/adapters/gemini/GeminiAdapter.d.ts.map +0 -1
  121. package/packages/agents/src/adapters/gemini/GeminiAdapter.js +0 -42
  122. package/packages/agents/src/adapters/gemini/GeminiAdapter.ts +0 -58
  123. package/packages/agents/src/adapters/gemini/GeminiCliRunner.ts +0 -75
  124. package/packages/agents/src/adapters/local/.gitkeep +0 -0
  125. package/packages/agents/src/adapters/local/LocalAdapter.d.ts +0 -11
  126. package/packages/agents/src/adapters/local/LocalAdapter.d.ts.map +0 -1
  127. package/packages/agents/src/adapters/local/LocalAdapter.js +0 -38
  128. package/packages/agents/src/adapters/local/LocalAdapter.ts +0 -43
  129. package/packages/agents/src/adapters/ollama/OllamaCliAdapter.ts +0 -58
  130. package/packages/agents/src/adapters/ollama/OllamaCliRunner.ts +0 -70
  131. package/packages/agents/src/adapters/ollama/OllamaRemoteAdapter.ts +0 -205
  132. package/packages/agents/src/adapters/openai/.gitkeep +0 -0
  133. package/packages/agents/src/adapters/openai/OpenAiAdapter.d.ts +0 -11
  134. package/packages/agents/src/adapters/openai/OpenAiAdapter.d.ts.map +0 -1
  135. package/packages/agents/src/adapters/openai/OpenAiAdapter.js +0 -51
  136. package/packages/agents/src/adapters/openai/OpenAiAdapter.ts +0 -56
  137. package/packages/agents/src/adapters/openai/OpenAiCliAdapter.ts +0 -62
  138. package/packages/agents/src/adapters/qa/.gitkeep +0 -0
  139. package/packages/agents/src/adapters/qa/QaAdapter.d.ts +0 -11
  140. package/packages/agents/src/adapters/qa/QaAdapter.d.ts.map +0 -1
  141. package/packages/agents/src/adapters/qa/QaAdapter.js +0 -37
  142. package/packages/agents/src/adapters/qa/QaAdapter.ts +0 -42
  143. package/packages/agents/src/adapters/zhipu/ZhipuApiAdapter.ts +0 -273
  144. package/packages/agents/src/index.d.ts +0 -8
  145. package/packages/agents/src/index.d.ts.map +0 -1
  146. package/packages/agents/src/index.js +0 -7
  147. package/packages/agents/src/index.ts +0 -11
  148. package/packages/agents/tsconfig.json +0 -14
  149. package/packages/cli/CHANGELOG.md +0 -7
  150. package/packages/cli/LICENSE +0 -21
  151. package/packages/cli/README.md +0 -23
  152. package/packages/cli/package.json +0 -61
  153. package/packages/cli/src/__tests__/AgentsCommands.test.ts +0 -137
  154. package/packages/cli/src/__tests__/BacklogCommands.test.ts +0 -40
  155. package/packages/cli/src/__tests__/CodeReviewCommand.test.ts +0 -594
  156. package/packages/cli/src/__tests__/CreateTasksCommand.test.ts +0 -40
  157. package/packages/cli/src/__tests__/DocsCommands.test.ts +0 -41
  158. package/packages/cli/src/__tests__/EstimateCommands.test.ts +0 -54
  159. package/packages/cli/src/__tests__/JobsCommands.behavior.test.ts +0 -311
  160. package/packages/cli/src/__tests__/JobsCommands.test.ts +0 -49
  161. package/packages/cli/src/__tests__/MigrateTasksCommand.test.ts +0 -36
  162. package/packages/cli/src/__tests__/OpenapiCommands.test.ts +0 -34
  163. package/packages/cli/src/__tests__/OrderTasksCommand.test.ts +0 -150
  164. package/packages/cli/src/__tests__/PlanningCommands.test.ts +0 -9
  165. package/packages/cli/src/__tests__/QaTasksCommand.test.ts +0 -58
  166. package/packages/cli/src/__tests__/RefineTasksCommand.test.ts +0 -63
  167. package/packages/cli/src/__tests__/RoutingCommands.test.ts +0 -302
  168. package/packages/cli/src/__tests__/SetWorkspaceCommand.test.ts +0 -18
  169. package/packages/cli/src/__tests__/TaskShowCommands.test.ts +0 -130
  170. package/packages/cli/src/__tests__/TelemetryCommands.test.ts +0 -35
  171. package/packages/cli/src/__tests__/TestAgentCommand.test.ts +0 -41
  172. package/packages/cli/src/__tests__/UpdateCommands.test.ts +0 -292
  173. package/packages/cli/src/__tests__/WorkOnTasksCommand.test.ts +0 -42
  174. package/packages/cli/src/bin/.gitkeep +0 -0
  175. package/packages/cli/src/bin/McodaEntrypoint.ts +0 -180
  176. package/packages/cli/src/commands/agents/.gitkeep +0 -0
  177. package/packages/cli/src/commands/agents/AgentsCommands.ts +0 -374
  178. package/packages/cli/src/commands/agents/GatewayAgentCommand.ts +0 -621
  179. package/packages/cli/src/commands/agents/TestAgentCommand.ts +0 -63
  180. package/packages/cli/src/commands/backlog/.gitkeep +0 -0
  181. package/packages/cli/src/commands/backlog/BacklogCommands.ts +0 -286
  182. package/packages/cli/src/commands/backlog/OrderTasksCommand.ts +0 -237
  183. package/packages/cli/src/commands/backlog/TaskShowCommands.ts +0 -289
  184. package/packages/cli/src/commands/docs/.gitkeep +0 -0
  185. package/packages/cli/src/commands/docs/DocsCommands.ts +0 -413
  186. package/packages/cli/src/commands/estimate/EstimateCommands.ts +0 -290
  187. package/packages/cli/src/commands/jobs/.gitkeep +0 -0
  188. package/packages/cli/src/commands/jobs/JobsCommands.ts +0 -595
  189. package/packages/cli/src/commands/openapi/OpenapiCommands.ts +0 -167
  190. package/packages/cli/src/commands/planning/.gitkeep +0 -0
  191. package/packages/cli/src/commands/planning/CreateTasksCommand.ts +0 -149
  192. package/packages/cli/src/commands/planning/MigrateTasksCommand.ts +0 -105
  193. package/packages/cli/src/commands/planning/PlanningCommands.ts +0 -1
  194. package/packages/cli/src/commands/planning/QaTasksCommand.ts +0 -320
  195. package/packages/cli/src/commands/planning/RefineTasksCommand.ts +0 -408
  196. package/packages/cli/src/commands/review/CodeReviewCommand.ts +0 -262
  197. package/packages/cli/src/commands/routing/.gitkeep +0 -0
  198. package/packages/cli/src/commands/routing/RoutingCommands.ts +0 -554
  199. package/packages/cli/src/commands/telemetry/.gitkeep +0 -0
  200. package/packages/cli/src/commands/telemetry/TelemetryCommands.ts +0 -348
  201. package/packages/cli/src/commands/update/.gitkeep +0 -0
  202. package/packages/cli/src/commands/update/UpdateCommands.ts +0 -301
  203. package/packages/cli/src/commands/work/WorkOnTasksCommand.ts +0 -264
  204. package/packages/cli/src/commands/workspace/SetWorkspaceCommand.ts +0 -132
  205. package/packages/cli/test/packaging_guardrails.test.js +0 -75
  206. package/packages/cli/tsconfig.json +0 -20
  207. package/packages/core/CHANGELOG.md +0 -7
  208. package/packages/core/LICENSE +0 -21
  209. package/packages/core/README.md +0 -9
  210. package/packages/core/package.json +0 -45
  211. package/packages/core/src/__tests__/SmokeClasses.test.ts +0 -32
  212. package/packages/core/src/api/AgentsApi.ts +0 -219
  213. package/packages/core/src/api/QaTasksApi.ts +0 -38
  214. package/packages/core/src/api/TasksApi.ts +0 -35
  215. package/packages/core/src/api/__tests__/AgentsApi.test.ts +0 -203
  216. package/packages/core/src/api/__tests__/QaTasksApi.test.ts +0 -51
  217. package/packages/core/src/api/__tests__/TasksApi.test.ts +0 -56
  218. package/packages/core/src/config/.gitkeep +0 -0
  219. package/packages/core/src/config/ConfigService.ts +0 -1
  220. package/packages/core/src/domain/dependencies/.gitkeep +0 -0
  221. package/packages/core/src/domain/dependencies/Dependency.ts +0 -1
  222. package/packages/core/src/domain/epics/.gitkeep +0 -0
  223. package/packages/core/src/domain/epics/Epic.ts +0 -1
  224. package/packages/core/src/domain/projects/.gitkeep +0 -0
  225. package/packages/core/src/domain/projects/Project.ts +0 -1
  226. package/packages/core/src/domain/tasks/.gitkeep +0 -0
  227. package/packages/core/src/domain/tasks/Task.ts +0 -1
  228. package/packages/core/src/domain/userStories/.gitkeep +0 -0
  229. package/packages/core/src/domain/userStories/UserStory.ts +0 -1
  230. package/packages/core/src/index.ts +0 -27
  231. package/packages/core/src/prompts/.gitkeep +0 -0
  232. package/packages/core/src/prompts/PdrPrompts.ts +0 -23
  233. package/packages/core/src/prompts/PromptLoader.ts +0 -1
  234. package/packages/core/src/prompts/SdsPrompts.ts +0 -47
  235. package/packages/core/src/services/agents/.gitkeep +0 -0
  236. package/packages/core/src/services/agents/AgentManagementService.ts +0 -1
  237. package/packages/core/src/services/agents/GatewayAgentService.ts +0 -956
  238. package/packages/core/src/services/agents/RoutingService.ts +0 -461
  239. package/packages/core/src/services/agents/__tests__/GatewayAgentService.test.ts +0 -72
  240. package/packages/core/src/services/agents/__tests__/RoutingService.test.ts +0 -267
  241. package/packages/core/src/services/agents/generated/RoutingApiClient.ts +0 -89
  242. package/packages/core/src/services/backlog/.gitkeep +0 -0
  243. package/packages/core/src/services/backlog/BacklogService.ts +0 -580
  244. package/packages/core/src/services/backlog/TaskOrderingService.ts +0 -868
  245. package/packages/core/src/services/backlog/__tests__/BacklogService.test.ts +0 -219
  246. package/packages/core/src/services/backlog/__tests__/TaskOrderingService.test.ts +0 -268
  247. package/packages/core/src/services/docs/.gitkeep +0 -0
  248. package/packages/core/src/services/docs/DocsService.ts +0 -1913
  249. package/packages/core/src/services/docs/__tests__/DocsService.test.ts +0 -350
  250. package/packages/core/src/services/estimate/EstimateService.ts +0 -111
  251. package/packages/core/src/services/estimate/VelocityService.ts +0 -272
  252. package/packages/core/src/services/estimate/__tests__/VelocityAndEstimate.test.ts +0 -209
  253. package/packages/core/src/services/estimate/types.ts +0 -41
  254. package/packages/core/src/services/execution/.gitkeep +0 -0
  255. package/packages/core/src/services/execution/ExecutionService.ts +0 -1
  256. package/packages/core/src/services/execution/QaFollowupService.ts +0 -289
  257. package/packages/core/src/services/execution/QaProfileService.ts +0 -160
  258. package/packages/core/src/services/execution/QaTasksService.ts +0 -1303
  259. package/packages/core/src/services/execution/TaskSelectionService.ts +0 -362
  260. package/packages/core/src/services/execution/TaskStateService.ts +0 -64
  261. package/packages/core/src/services/execution/WorkOnTasksService.ts +0 -2023
  262. package/packages/core/src/services/execution/__tests__/QaFollowupService.test.ts +0 -58
  263. package/packages/core/src/services/execution/__tests__/QaProfileService.test.ts +0 -49
  264. package/packages/core/src/services/execution/__tests__/QaTasksService.test.ts +0 -157
  265. package/packages/core/src/services/execution/__tests__/TaskSelectionService.test.ts +0 -179
  266. package/packages/core/src/services/execution/__tests__/TaskStateService.test.ts +0 -51
  267. package/packages/core/src/services/execution/__tests__/WorkOnTasksService.test.ts +0 -285
  268. package/packages/core/src/services/jobs/.gitkeep +0 -0
  269. package/packages/core/src/services/jobs/JobInsightsService.ts +0 -355
  270. package/packages/core/src/services/jobs/JobResumeService.ts +0 -119
  271. package/packages/core/src/services/jobs/JobService.ts +0 -648
  272. package/packages/core/src/services/jobs/JobsApiClient.ts +0 -113
  273. package/packages/core/src/services/jobs/__tests__/JobInsightsService.test.ts +0 -17
  274. package/packages/core/src/services/jobs/__tests__/JobResumeService.test.ts +0 -45
  275. package/packages/core/src/services/jobs/__tests__/JobService.test.ts +0 -44
  276. package/packages/core/src/services/openapi/OpenApiService.ts +0 -558
  277. package/packages/core/src/services/openapi/__tests__/OpenApiService.test.ts +0 -57
  278. package/packages/core/src/services/planning/.gitkeep +0 -0
  279. package/packages/core/src/services/planning/CreateTasksService.ts +0 -1280
  280. package/packages/core/src/services/planning/KeyHelpers.ts +0 -80
  281. package/packages/core/src/services/planning/PlanningService.ts +0 -1
  282. package/packages/core/src/services/planning/RefineTasksService.ts +0 -1552
  283. package/packages/core/src/services/planning/__tests__/CreateTasksService.test.ts +0 -288
  284. package/packages/core/src/services/planning/__tests__/KeyHelpers.test.ts +0 -16
  285. package/packages/core/src/services/planning/__tests__/RefineTasksService.test.ts +0 -172
  286. package/packages/core/src/services/review/CodeReviewService.ts +0 -1386
  287. package/packages/core/src/services/review/__tests__/CodeReviewService.test.ts +0 -89
  288. package/packages/core/src/services/system/SystemUpdateService.ts +0 -177
  289. package/packages/core/src/services/system/__tests__/SystemUpdateService.test.ts +0 -40
  290. package/packages/core/src/services/tasks/TaskApiResolver.ts +0 -37
  291. package/packages/core/src/services/tasks/TaskDetailService.ts +0 -494
  292. package/packages/core/src/services/tasks/__tests__/TaskApiResolver.test.ts +0 -41
  293. package/packages/core/src/services/tasks/__tests__/TaskDetailService.test.ts +0 -178
  294. package/packages/core/src/services/telemetry/.gitkeep +0 -0
  295. package/packages/core/src/services/telemetry/TelemetryService.ts +0 -515
  296. package/packages/core/src/services/telemetry/__tests__/TelemetryService.test.ts +0 -160
  297. package/packages/core/src/workspace/.gitkeep +0 -0
  298. package/packages/core/src/workspace/WorkspaceManager.ts +0 -234
  299. package/packages/core/tsconfig.json +0 -20
  300. package/packages/db/CHANGELOG.md +0 -7
  301. package/packages/db/LICENSE +0 -21
  302. package/packages/db/README.md +0 -9
  303. package/packages/db/package.json +0 -42
  304. package/packages/db/src/__tests__/GlobalRepository.test.ts +0 -109
  305. package/packages/db/src/__tests__/SchemaAlignment.test.ts +0 -80
  306. package/packages/db/src/__tests__/WorkspaceRepository.test.ts +0 -19
  307. package/packages/db/src/index.d.ts +0 -6
  308. package/packages/db/src/index.d.ts.map +0 -1
  309. package/packages/db/src/index.js +0 -5
  310. package/packages/db/src/index.ts +0 -6
  311. package/packages/db/src/migrations/global/.gitkeep +0 -0
  312. package/packages/db/src/migrations/global/GlobalMigrations.d.ts +0 -9
  313. package/packages/db/src/migrations/global/GlobalMigrations.d.ts.map +0 -1
  314. package/packages/db/src/migrations/global/GlobalMigrations.js +0 -68
  315. package/packages/db/src/migrations/global/GlobalMigrations.ts +0 -336
  316. package/packages/db/src/migrations/workspace/.gitkeep +0 -0
  317. package/packages/db/src/migrations/workspace/WorkspaceMigrations.d.ts +0 -9
  318. package/packages/db/src/migrations/workspace/WorkspaceMigrations.d.ts.map +0 -1
  319. package/packages/db/src/migrations/workspace/WorkspaceMigrations.js +0 -251
  320. package/packages/db/src/migrations/workspace/WorkspaceMigrations.ts +0 -248
  321. package/packages/db/src/repositories/global/.gitkeep +0 -0
  322. package/packages/db/src/repositories/global/GlobalRepository.d.ts +0 -30
  323. package/packages/db/src/repositories/global/GlobalRepository.d.ts.map +0 -1
  324. package/packages/db/src/repositories/global/GlobalRepository.js +0 -209
  325. package/packages/db/src/repositories/global/GlobalRepository.ts +0 -492
  326. package/packages/db/src/repositories/workspace/.gitkeep +0 -0
  327. package/packages/db/src/repositories/workspace/WorkspaceRepository.d.ts +0 -282
  328. package/packages/db/src/repositories/workspace/WorkspaceRepository.d.ts.map +0 -1
  329. package/packages/db/src/repositories/workspace/WorkspaceRepository.js +0 -773
  330. package/packages/db/src/repositories/workspace/WorkspaceRepository.ts +0 -1511
  331. package/packages/db/src/sqlite/connection.d.ts +0 -11
  332. package/packages/db/src/sqlite/connection.d.ts.map +0 -1
  333. package/packages/db/src/sqlite/connection.js +0 -31
  334. package/packages/db/src/sqlite/connection.ts +0 -35
  335. package/packages/db/src/sqlite/pragmas.d.ts +0 -5
  336. package/packages/db/src/sqlite/pragmas.d.ts.map +0 -1
  337. package/packages/db/src/sqlite/pragmas.js +0 -6
  338. package/packages/db/src/sqlite/pragmas.ts +0 -10
  339. package/packages/db/tsconfig.json +0 -13
  340. package/packages/generators/package.json +0 -21
  341. package/packages/generators/src/__tests__/Generators.test.ts +0 -19
  342. package/packages/generators/src/index.ts +0 -1
  343. package/packages/generators/src/openapi/generateTypes.ts +0 -1
  344. package/packages/generators/src/openapi/validateSchema.ts +0 -1
  345. package/packages/generators/src/scaffolding/docs/.gitkeep +0 -0
  346. package/packages/generators/src/scaffolding/docs/DocsScaffolder.ts +0 -1
  347. package/packages/generators/src/scaffolding/global/.gitkeep +0 -0
  348. package/packages/generators/src/scaffolding/global/GlobalScaffolder.ts +0 -1
  349. package/packages/generators/src/scaffolding/workspace/.gitkeep +0 -0
  350. package/packages/generators/src/scaffolding/workspace/WorkspaceScaffolder.ts +0 -1
  351. package/packages/generators/tsconfig.json +0 -10
  352. package/packages/integrations/CHANGELOG.md +0 -7
  353. package/packages/integrations/LICENSE +0 -21
  354. package/packages/integrations/README.md +0 -9
  355. package/packages/integrations/package.json +0 -47
  356. package/packages/integrations/src/docdex/.gitkeep +0 -0
  357. package/packages/integrations/src/docdex/DocdexClient.d.ts +0 -50
  358. package/packages/integrations/src/docdex/DocdexClient.d.ts.map +0 -1
  359. package/packages/integrations/src/docdex/DocdexClient.js +0 -216
  360. package/packages/integrations/src/docdex/DocdexClient.ts +0 -261
  361. package/packages/integrations/src/docdex/__tests__/DocdexClient.test.ts +0 -29
  362. package/packages/integrations/src/index.d.ts +0 -2
  363. package/packages/integrations/src/index.d.ts.map +0 -1
  364. package/packages/integrations/src/index.js +0 -4
  365. package/packages/integrations/src/index.ts +0 -5
  366. package/packages/integrations/src/issues/.gitkeep +0 -0
  367. package/packages/integrations/src/issues/IssuesClient.ts +0 -1
  368. package/packages/integrations/src/issues/__tests__/IssuesClient.test.ts +0 -10
  369. package/packages/integrations/src/qa/.gitkeep +0 -0
  370. package/packages/integrations/src/qa/ChromiumQaAdapter.ts +0 -89
  371. package/packages/integrations/src/qa/CliQaAdapter.ts +0 -95
  372. package/packages/integrations/src/qa/MaestroQaAdapter.ts +0 -91
  373. package/packages/integrations/src/qa/QaAdapter.ts +0 -7
  374. package/packages/integrations/src/qa/QaClient.ts +0 -1
  375. package/packages/integrations/src/qa/QaTypes.ts +0 -26
  376. package/packages/integrations/src/qa/__tests__/ChromiumQaAdapter.test.ts +0 -30
  377. package/packages/integrations/src/qa/__tests__/CliQaAdapter.test.ts +0 -33
  378. package/packages/integrations/src/qa/__tests__/MaestroQaAdapter.test.ts +0 -30
  379. package/packages/integrations/src/qa/index.ts +0 -5
  380. package/packages/integrations/src/system/SystemClient.ts +0 -50
  381. package/packages/integrations/src/system/__tests__/SystemClient.test.ts +0 -40
  382. package/packages/integrations/src/telemetry/TelemetryClient.ts +0 -139
  383. package/packages/integrations/src/telemetry/__tests__/TelemetryClient.test.ts +0 -41
  384. package/packages/integrations/src/vcs/.gitkeep +0 -0
  385. package/packages/integrations/src/vcs/VcsClient.ts +0 -211
  386. package/packages/integrations/src/vcs/__tests__/VcsClient.test.ts +0 -26
  387. package/packages/integrations/tsconfig.json +0 -14
  388. package/packages/shared/CHANGELOG.md +0 -7
  389. package/packages/shared/LICENSE +0 -21
  390. package/packages/shared/README.md +0 -9
  391. package/packages/shared/package.json +0 -40
  392. package/packages/shared/src/__tests__/CommandMetadata.test.ts +0 -15
  393. package/packages/shared/src/__tests__/ServiceShells.test.ts +0 -16
  394. package/packages/shared/src/crypto/.gitkeep +0 -0
  395. package/packages/shared/src/crypto/CryptoHelper.d.ts +0 -15
  396. package/packages/shared/src/crypto/CryptoHelper.d.ts.map +0 -1
  397. package/packages/shared/src/crypto/CryptoHelper.js +0 -54
  398. package/packages/shared/src/crypto/CryptoHelper.ts +0 -57
  399. package/packages/shared/src/errors/.gitkeep +0 -0
  400. package/packages/shared/src/errors/ErrorFactory.ts +0 -1
  401. package/packages/shared/src/index.d.ts +0 -6
  402. package/packages/shared/src/index.d.ts.map +0 -1
  403. package/packages/shared/src/index.js +0 -4
  404. package/packages/shared/src/index.ts +0 -35
  405. package/packages/shared/src/logging/.gitkeep +0 -0
  406. package/packages/shared/src/logging/Logger.ts +0 -1
  407. package/packages/shared/src/metadata/CommandMetadata.ts +0 -165
  408. package/packages/shared/src/openapi/.gitkeep +0 -0
  409. package/packages/shared/src/openapi/OpenApiTypes.d.ts +0 -216
  410. package/packages/shared/src/openapi/OpenApiTypes.d.ts.map +0 -1
  411. package/packages/shared/src/openapi/OpenApiTypes.js +0 -1
  412. package/packages/shared/src/openapi/OpenApiTypes.ts +0 -312
  413. package/packages/shared/src/paths/.gitkeep +0 -0
  414. package/packages/shared/src/paths/PathHelper.d.ts +0 -12
  415. package/packages/shared/src/paths/PathHelper.d.ts.map +0 -1
  416. package/packages/shared/src/paths/PathHelper.js +0 -24
  417. package/packages/shared/src/paths/PathHelper.ts +0 -29
  418. package/packages/shared/src/qa/QaProfile.ts +0 -14
  419. package/packages/shared/src/utils/.gitkeep +0 -0
  420. package/packages/shared/src/utils/UtilityService.ts +0 -1
  421. package/packages/shared/tsconfig.json +0 -10
  422. package/packages/testing/package.json +0 -26
  423. package/packages/testing/src/__tests__/TestingFakes.test.ts +0 -15
  424. package/packages/testing/src/cli/e2e/.gitkeep +0 -0
  425. package/packages/testing/src/cli/e2e/E2eSuite.ts +0 -1
  426. package/packages/testing/src/fakes/agents/.gitkeep +0 -0
  427. package/packages/testing/src/fakes/agents/FakeAgents.ts +0 -1
  428. package/packages/testing/src/fakes/docdex/.gitkeep +0 -0
  429. package/packages/testing/src/fakes/docdex/FakeDocdexClient.ts +0 -1
  430. package/packages/testing/src/fakes/qa/.gitkeep +0 -0
  431. package/packages/testing/src/fakes/qa/FakeQaClient.ts +0 -1
  432. package/packages/testing/src/fakes/vcs/.gitkeep +0 -0
  433. package/packages/testing/src/fakes/vcs/FakeVcsClient.ts +0 -1
  434. package/packages/testing/src/fixtures/db/.gitkeep +0 -0
  435. package/packages/testing/src/fixtures/db/DbFixtures.ts +0 -1
  436. package/packages/testing/src/fixtures/workspaces/.gitkeep +0 -0
  437. package/packages/testing/src/fixtures/workspaces/WorkspaceFixtures.ts +0 -1
  438. package/packages/testing/src/index.ts +0 -1
  439. package/packages/testing/tsconfig.json +0 -10
  440. package/pnpm-workspace.yaml +0 -2
  441. package/prompts/README.md +0 -5
  442. package/prompts/code-reviewer.md +0 -23
  443. package/prompts/code-writer.md +0 -35
  444. package/prompts/gateway-agent.md +0 -27
  445. package/prompts/qa-agent.md +0 -21
  446. package/release-please-config.json +0 -39
  447. package/scripts/build-all.ts +0 -1
  448. package/scripts/dev.ts +0 -1
  449. package/scripts/install-local-cli.sh +0 -28
  450. package/scripts/pack-npm-tarballs.js +0 -63
  451. package/scripts/release.ts +0 -1
  452. package/scripts/run-node-tests.js +0 -37
  453. package/tests/all.js +0 -127
  454. package/tests/api/openapi_spec.test.js +0 -21
  455. package/tests/artifacts.md +0 -31
  456. package/tests/component/cli_version.test.js +0 -38
  457. package/tests/integration/workspace_resolver.test.js +0 -44
  458. package/tests/unit/crypto_helper.test.js +0 -36
  459. package/tests/unit/path_helper.test.js +0 -20
  460. package/tsconfig.base.json +0 -32
  461. /package/{packages/cli/src/index.ts → dist/index.js} +0 -0
@@ -1,2023 +0,0 @@
1
- import { exec as execCb } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import path from "node:path";
4
- import fs from "node:fs";
5
- import { AgentService } from "@mcoda/agents";
6
- import { DocdexClient, VcsClient } from "@mcoda/integrations";
7
- import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
8
- import { PathHelper } from "@mcoda/shared";
9
- import { WorkspaceResolution } from "../../workspace/WorkspaceManager.js";
10
- import { JobService, type JobState } from "../jobs/JobService.js";
11
- import { TaskSelectionService, TaskSelectionFilters, TaskSelectionPlan } from "./TaskSelectionService.js";
12
- import { TaskStateService } from "./TaskStateService.js";
13
- import { RoutingService } from "../agents/RoutingService.js";
14
-
15
- const exec = promisify(execCb);
16
- const DEFAULT_BASE_BRANCH = "mcoda-dev";
17
- const DEFAULT_TASK_BRANCH_PREFIX = "mcoda/task/";
18
- const TASK_LOCK_TTL_SECONDS = 60 * 60;
19
- const DEFAULT_CODE_WRITER_PROMPT = [
20
- "You are the code-writing agent. Before coding, query docdex with the task key and feature keywords (MCP `docdex_search` limit 4–8 or CLI `docdexd query --repo <repo> --query \"<term>\" --limit 6 --snippets=false`). If results look stale, reindex (`docdex_index` or `docdexd index --repo <repo>`) then re-run search. Fetch snippets via `docdex_open` or `/snippet/:doc_id?text_only=true` only for specific hits.",
21
- "Use docdex snippets to ground decisions (data model, offline/online expectations, constraints, acceptance criteria). Note when docdex is unavailable and fall back to local docs.",
22
- "Re-use existing store/slices/adapters and tests; avoid inventing new backends or ad-hoc actions. Keep behavior backward-compatible and scoped to the documented contracts.",
23
- "If you encounter merge conflicts, resolve them first (clean conflict markers and ensure code compiles) before continuing task work.",
24
- "If a target file does not exist, create it by outputting a FILE block (not a diff): `FILE: path/to/file.ext` followed by a fenced code block containing the full file contents.",
25
- ].join("\n");
26
- const DEFAULT_JOB_PROMPT = "You are an mcoda agent that follows workspace runbooks and responds with actionable, concise output.";
27
- const DEFAULT_CHARACTER_PROMPT =
28
- "Write clearly, avoid hallucinations, cite assumptions, and prioritize risk mitigation for the user.";
29
-
30
- export interface WorkOnTasksRequest extends TaskSelectionFilters {
31
- workspace: WorkspaceResolution;
32
- noCommit?: boolean;
33
- dryRun?: boolean;
34
- agentName?: string;
35
- agentStream?: boolean;
36
- baseBranch?: string;
37
- onAgentChunk?: (chunk: string) => void;
38
- }
39
-
40
- export interface TaskExecutionResult {
41
- taskKey: string;
42
- status: "succeeded" | "blocked" | "failed" | "skipped";
43
- notes?: string;
44
- branch?: string;
45
- }
46
-
47
- export interface WorkOnTasksResult {
48
- jobId: string;
49
- commandRunId: string;
50
- selection: TaskSelectionPlan;
51
- results: TaskExecutionResult[];
52
- warnings: string[];
53
- }
54
-
55
- const estimateTokens = (text: string): number => Math.max(1, Math.ceil((text ?? "").length / 4));
56
-
57
- const extractPatches = (output: string): string[] => {
58
- const matches = [...output.matchAll(/```(?:patch|diff)[\s\S]*?```/g)];
59
- return matches.map((m) => m[0].replace(/```(?:patch|diff)/, "").replace(/```$/, "").trim()).filter(Boolean);
60
- };
61
-
62
- const extractFileBlocks = (output: string): Array<{ path: string; content: string }> => {
63
- const files: Array<{ path: string; content: string }> = [];
64
- const regex = /(?:^|\r?\n)FILE:\s*([^\r\n]+)\r?\n```[^\r\n]*\r?\n([\s\S]*?)\r?\n```/g;
65
- let match: RegExpExecArray | null;
66
- while ((match = regex.exec(output)) !== null) {
67
- const filePath = match[1]?.trim();
68
- if (!filePath) continue;
69
- files.push({ path: filePath, content: match[2] ?? "" });
70
- }
71
- return files;
72
- };
73
-
74
- type TaskPhase = "selection" | "context" | "prompt" | "agent" | "apply" | "tests" | "vcs" | "finalize";
75
-
76
- const touchedFilesFromPatch = (patch: string): string[] => {
77
- const files = new Set<string>();
78
- const regex = /^\+\+\+\s+b\/([^\s]+)/gm;
79
- let match: RegExpExecArray | null;
80
- while ((match = regex.exec(patch)) !== null) {
81
- files.add(match[1]);
82
- }
83
- return Array.from(files);
84
- };
85
-
86
- const normalizePaths = (workspaceRoot: string, files: string[]): string[] =>
87
- files.map((f) => path.relative(workspaceRoot, path.isAbsolute(f) ? f : path.join(workspaceRoot, f))).map((f) => f.replace(/\\/g, "/"));
88
- const MCODA_GITIGNORE_ENTRY = ".mcoda/\n";
89
- const WORK_DIR = (jobId: string, workspaceRoot: string) => path.join(workspaceRoot, ".mcoda", "jobs", jobId, "work");
90
-
91
- const maybeConvertApplyPatch = (patch: string): string => {
92
- if (!patch.trimStart().startsWith("*** Begin Patch")) return patch;
93
- const lines = patch.split(/\r?\n/);
94
- let i = 0;
95
- const out: string[] = [];
96
- const next = () => lines[++i];
97
- const current = () => lines[i];
98
- const advanceUntilNextFile = () => {
99
- while (i < lines.length && !current().startsWith("*** ")) i += 1;
100
- };
101
-
102
- while (i < lines.length) {
103
- const line = current();
104
- if (line.startsWith("*** Begin Patch") || line.startsWith("*** End Patch")) {
105
- i += 1;
106
- continue;
107
- }
108
- if (line.startsWith("*** Add File: ")) {
109
- const file = line.replace("*** Add File: ", "").trim();
110
- const content: string[] = [];
111
- i += 1;
112
- while (i < lines.length && !current().startsWith("*** ")) {
113
- const l = current();
114
- if (l.startsWith("+")) {
115
- content.push(l.slice(1));
116
- } else if (!l.startsWith("\")) {
117
- // Some apply_patch emitters omit the leading "+", so treat raw lines as content.
118
- content.push(l);
119
- }
120
- i += 1;
121
- }
122
- const count = content.length;
123
- out.push(`diff --git a/${file} b/${file}`);
124
- out.push("new file mode 100644");
125
- out.push("--- /dev/null");
126
- out.push(`+++ b/${file}`);
127
- if (count > 0) {
128
- out.push(`@@ -0,0 +1,${count} @@`);
129
- content.forEach((l) => out.push(`+${l}`));
130
- } else {
131
- out.push("@@ -0,0 +0,0 @@");
132
- }
133
- continue;
134
- }
135
- if (line.startsWith("*** Delete File: ")) {
136
- const file = line.replace("*** Delete File: ", "").trim();
137
- out.push(`diff --git a/${file} b/${file}`);
138
- out.push("deleted file mode 100644");
139
- out.push(`--- a/${file}`);
140
- out.push("+++ /dev/null");
141
- out.push("@@ -1 +0,0 @@");
142
- i += 1;
143
- advanceUntilNextFile();
144
- continue;
145
- }
146
- if (line.startsWith("*** Update File: ")) {
147
- const file = line.replace("*** Update File: ", "").trim();
148
- i += 1;
149
- // Skip optional move line
150
- if (i < lines.length && current().startsWith("*** Move to: ")) i += 1;
151
- out.push(`diff --git a/${file} b/${file}`);
152
- out.push(`--- a/${file}`);
153
- out.push(`+++ b/${file}`);
154
- while (i < lines.length && !current().startsWith("*** ")) {
155
- const l = current();
156
- if (l.startsWith("@@") || l.startsWith("+++") || l.startsWith("---") || l.startsWith("+") || l.startsWith("-") || l.startsWith(" ")) {
157
- out.push(l);
158
- }
159
- i += 1;
160
- }
161
- continue;
162
- }
163
- i += 1;
164
- }
165
- return out.join("\n");
166
- };
167
-
168
- const ensureDiffHeader = (patch: string): string => {
169
- const lines = patch.split(/\r?\n/);
170
- const hasHeader = /^diff --git /m.test(patch);
171
- const minusIdx = lines.findIndex((l) => l.startsWith("--- "));
172
- const plusIdx = lines.findIndex((l) => l.startsWith("+++ "));
173
- if (minusIdx === -1 || plusIdx === -1) return patch;
174
- const minusPathRaw = lines[minusIdx].replace(/^---\s+/, "").trim();
175
- const plusPathRaw = lines[plusIdx].replace(/^\+\+\+\s+/, "").trim();
176
- const lhs =
177
- minusPathRaw === "/dev/null"
178
- ? plusPathRaw.replace(/^b\//, "")
179
- : minusPathRaw.replace(/^a\//, "");
180
- const rhs = plusPathRaw.replace(/^b\//, "");
181
- const header = `diff --git a/${lhs} b/${rhs}`;
182
- const result: string[] = [...lines];
183
- if (!hasHeader) {
184
- result.unshift(header);
185
- }
186
- const headerIdx = result.findIndex((l) => l.startsWith("diff --git "));
187
- const hasNewFileMode = result.some((l) => l.startsWith("new file mode"));
188
- const isAdd = minusPathRaw === "/dev/null";
189
- if (isAdd && !hasNewFileMode) {
190
- result.splice(headerIdx + 1, 0, "new file mode 100644");
191
- }
192
- return result.join("\n");
193
- };
194
-
195
- const stripInvalidIndexLines = (patch: string): string =>
196
- patch
197
- .split(/\r?\n/)
198
- .filter((line) => {
199
- if (!line.startsWith("index ")) return true;
200
- const value = line.replace(/^index\s+/, "").trim();
201
- return /^[0-9a-f]{7,40}\.\.[0-9a-f]{7,40}$/.test(value);
202
- })
203
- .join("\n");
204
-
205
- const isPlaceholderPatch = (patch: string): boolean => /\?\?\?/.test(patch) || /rest of existing code/i.test(patch);
206
-
207
- const normalizeHunkHeaders = (patch: string): string => {
208
- const lines = patch.split(/\r?\n/);
209
- const out: string[] = [];
210
- let currentAddFile = false;
211
-
212
- const countLines = (start: number): { minus: number; plus: number } => {
213
- let minus = 0;
214
- let plus = 0;
215
- for (let j = start; j < lines.length; j += 1) {
216
- const l = lines[j];
217
- if (l.startsWith("@@") || l.startsWith("diff --git ") || l.startsWith("*** End Patch")) break;
218
- if (l.startsWith("+++ ") || l.startsWith("--- ")) continue;
219
- if (l.startsWith(" ")) {
220
- minus += 1;
221
- plus += 1;
222
- } else if (l.startsWith("-")) {
223
- minus += 1;
224
- } else if (l.startsWith("+")) {
225
- plus += 1;
226
- } else if (!l.trim()) {
227
- minus += 1;
228
- plus += 1;
229
- } else {
230
- break;
231
- }
232
- }
233
- return { minus, plus };
234
- };
235
-
236
- for (let i = 0; i < lines.length; i += 1) {
237
- const line = lines[i];
238
-
239
- if (line.startsWith("diff --git ")) {
240
- currentAddFile = false;
241
- out.push(line);
242
- continue;
243
- }
244
- if (line.startsWith("--- ")) {
245
- currentAddFile = line.includes("/dev/null");
246
- out.push(line);
247
- continue;
248
- }
249
- if (line.startsWith("+++ ")) {
250
- out.push(line);
251
- continue;
252
- }
253
-
254
- const isHunk = line.startsWith("@@");
255
- const hasRanges = /^@@\s+-\d+/.test(line);
256
- if (isHunk && !hasRanges) {
257
- const { minus, plus } = countLines(i + 1);
258
- const minusCount = currentAddFile ? 0 : minus;
259
- const plusCount = currentAddFile ? Math.max(plus, 0) : plus;
260
- out.push(`@@ -0,${minusCount} +1,${plusCount} @@`);
261
- continue;
262
- }
263
- out.push(line);
264
- }
265
- return out.join("\n");
266
- };
267
-
268
- const fixMissingPrefixesInHunks = (patch: string): string => {
269
- const lines = patch.split(/\r?\n/);
270
- const out: string[] = [];
271
- let inHunk = false;
272
- for (const line of lines) {
273
- if (line.startsWith("@@")) {
274
- inHunk = true;
275
- out.push(line);
276
- continue;
277
- }
278
- if (inHunk) {
279
- if (line.startsWith("diff --git ") || line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("*** End Patch")) {
280
- inHunk = false;
281
- out.push(line);
282
- continue;
283
- }
284
- if (!/^[+\-\s]/.test(line) && line.trim().length) {
285
- out.push(`+${line}`);
286
- continue;
287
- }
288
- }
289
- out.push(line);
290
- }
291
- return out.join("\n");
292
- };
293
-
294
- const parseAddedFileContents = (patch: string): Record<string, string> => {
295
- const lines = patch.split(/\r?\n/);
296
- const additions: Record<string, string[]> = {};
297
- let currentFile: string | null = null;
298
- let isAdd = false;
299
- for (let i = 0; i < lines.length; i += 1) {
300
- const line = lines[i];
301
- if (line.startsWith("diff --git ")) {
302
- currentFile = null;
303
- isAdd = false;
304
- }
305
- if (line.startsWith("--- ")) {
306
- const minusPath = line.replace(/^---\s+/, "").trim();
307
- isAdd = minusPath === "/dev/null";
308
- }
309
- if (line.startsWith("+++ ") && isAdd) {
310
- const plusPath = line.replace(/^\+\+\+\s+/, "").trim().replace(/^b\//, "");
311
- currentFile = plusPath;
312
- additions[currentFile] = [];
313
- }
314
- if (currentFile && isAdd) {
315
- if (line.startsWith("+") && !line.startsWith("+++")) {
316
- additions[currentFile].push(line.slice(1));
317
- }
318
- }
319
- }
320
- return Object.fromEntries(Object.entries(additions).map(([file, content]) => [file, content.join("\n")]));
321
- };
322
-
323
- const updateAddPatchForExistingFile = (patch: string, existingFiles: Set<string>, cwd: string): { patch: string; skipped: string[] } => {
324
- const additions = parseAddedFileContents(patch);
325
- const skipped: string[] = [];
326
- let updated = patch;
327
- for (const file of Object.keys(additions)) {
328
- const absolute = path.join(cwd, file);
329
- if (!existingFiles.has(absolute)) continue;
330
- try {
331
- const content = fs.readFileSync(absolute, "utf8");
332
- if (content.trim() === additions[file].trim()) {
333
- skipped.push(file);
334
- continue;
335
- }
336
- } catch {
337
- // ignore read errors; fall back to converting patch
338
- }
339
- // Convert add patch to update by removing new file mode and dev/null markers.
340
- const lines = updated.split(/\r?\n/);
341
- const out: string[] = [];
342
- for (let i = 0; i < lines.length; i += 1) {
343
- const line = lines[i];
344
- if (line.startsWith("diff --git ")) {
345
- out.push(line);
346
- continue;
347
- }
348
- if (line.startsWith("new file mode") && lines[i + 1]?.includes(file)) {
349
- continue;
350
- }
351
- if (line.startsWith("--- /dev/null") && lines[i + 1]?.includes(file)) {
352
- out.push(`--- a/${file}`);
353
- continue;
354
- }
355
- out.push(line);
356
- }
357
- updated = out.join("\n");
358
- }
359
- return { patch: updated, skipped };
360
- };
361
-
362
- const splitPatchIntoDiffs = (patch: string): string[] => {
363
- const parts = patch.split(/^diff --git /m).filter(Boolean);
364
- if (parts.length <= 1) return [patch];
365
- return parts.map((part) => `diff --git ${part}`.trim());
366
- };
367
-
368
- export class WorkOnTasksService {
369
- private selectionService: TaskSelectionService;
370
- private stateService: TaskStateService;
371
- private taskLogSeq = new Map<string, number>();
372
- private vcs: VcsClient;
373
- private routingService: RoutingService;
374
- private async readPromptFiles(paths: string[]): Promise<string[]> {
375
- const contents: string[] = [];
376
- const seen = new Set<string>();
377
- for (const promptPath of paths) {
378
- try {
379
- const content = await fs.promises.readFile(promptPath, "utf8");
380
- const trimmed = content.trim();
381
- if (trimmed && !seen.has(trimmed)) {
382
- contents.push(trimmed);
383
- seen.add(trimmed);
384
- }
385
- } catch {
386
- /* optional prompt */
387
- }
388
- }
389
- return contents;
390
- }
391
-
392
- constructor(
393
- private workspace: WorkspaceResolution,
394
- private deps: {
395
- agentService: AgentService;
396
- docdex: DocdexClient;
397
- jobService: JobService;
398
- workspaceRepo: WorkspaceRepository;
399
- selectionService?: TaskSelectionService;
400
- stateService?: TaskStateService;
401
- repo: GlobalRepository;
402
- vcsClient?: VcsClient;
403
- routingService: RoutingService;
404
- },
405
- ) {
406
- this.selectionService = deps.selectionService ?? new TaskSelectionService(workspace, deps.workspaceRepo);
407
- this.stateService = deps.stateService ?? new TaskStateService(deps.workspaceRepo);
408
- this.vcs = deps.vcsClient ?? new VcsClient();
409
- this.routingService = deps.routingService;
410
- }
411
-
412
- private async loadPrompts(agentId: string): Promise<{
413
- jobPrompt?: string;
414
- characterPrompt?: string;
415
- commandPrompt?: string;
416
- }> {
417
- const agentPrompts =
418
- "getPrompts" in this.deps.agentService ? await (this.deps.agentService as any).getPrompts(agentId) : undefined;
419
- const mcodaPromptPath = path.join(this.workspace.workspaceRoot, ".mcoda", "prompts", "code-writer.md");
420
- const workspacePromptPath = path.join(this.workspace.workspaceRoot, "prompts", "code-writer.md");
421
- try {
422
- await fs.promises.mkdir(path.dirname(mcodaPromptPath), { recursive: true });
423
- await fs.promises.access(mcodaPromptPath);
424
- console.info(`[work-on-tasks] using existing code-writer prompt at ${mcodaPromptPath}`);
425
- } catch {
426
- try {
427
- await fs.promises.access(workspacePromptPath);
428
- await fs.promises.copyFile(workspacePromptPath, mcodaPromptPath);
429
- console.info(`[work-on-tasks] copied code-writer prompt to ${mcodaPromptPath}`);
430
- } catch {
431
- console.info(`[work-on-tasks] no code-writer prompt found at ${workspacePromptPath}; writing default prompt to ${mcodaPromptPath}`);
432
- await fs.promises.writeFile(mcodaPromptPath, DEFAULT_CODE_WRITER_PROMPT, "utf8");
433
- }
434
- }
435
- const commandPromptFiles = await this.readPromptFiles([
436
- mcodaPromptPath,
437
- workspacePromptPath,
438
- ]);
439
- const mergedCommandPrompt = (() => {
440
- const parts = [...commandPromptFiles];
441
- if (agentPrompts?.commandPrompts?.["work-on-tasks"]) {
442
- parts.push(agentPrompts.commandPrompts["work-on-tasks"]);
443
- }
444
- if (!parts.length) parts.push(DEFAULT_CODE_WRITER_PROMPT);
445
- return parts.filter(Boolean).join("\n\n");
446
- })();
447
- return {
448
- jobPrompt: agentPrompts?.jobPrompt ?? DEFAULT_JOB_PROMPT,
449
- characterPrompt: agentPrompts?.characterPrompt ?? DEFAULT_CHARACTER_PROMPT,
450
- commandPrompt: mergedCommandPrompt || undefined,
451
- };
452
- }
453
-
454
- private async ensureMcoda(): Promise<void> {
455
- await PathHelper.ensureDir(this.workspace.mcodaDir);
456
- const gitignorePath = path.join(this.workspace.workspaceRoot, ".gitignore");
457
- try {
458
- const content = await fs.promises.readFile(gitignorePath, "utf8");
459
- if (!content.includes(".mcoda/")) {
460
- await fs.promises.writeFile(gitignorePath, `${content.trimEnd()}\n${MCODA_GITIGNORE_ENTRY}`, "utf8");
461
- }
462
- } catch {
463
- await fs.promises.writeFile(gitignorePath, MCODA_GITIGNORE_ENTRY, "utf8");
464
- }
465
- }
466
-
467
- private async writeWorkCheckpoint(jobId: string, data: Record<string, unknown>): Promise<void> {
468
- const dir = WORK_DIR(jobId, this.workspace.workspaceRoot);
469
- await fs.promises.mkdir(dir, { recursive: true });
470
- const target = path.join(dir, "state.json");
471
- await fs.promises.writeFile(target, JSON.stringify({ ...data, updatedAt: new Date().toISOString() }, null, 2), "utf8");
472
- }
473
-
474
- private async checkpoint(jobId: string, stage: string, details?: Record<string, unknown>): Promise<void> {
475
- const timestamp = new Date().toISOString();
476
- await this.deps.jobService.writeCheckpoint(jobId, {
477
- stage,
478
- timestamp,
479
- details,
480
- });
481
- await this.writeWorkCheckpoint(jobId, { stage, details, timestamp });
482
- }
483
-
484
- static async create(workspace: WorkspaceResolution): Promise<WorkOnTasksService> {
485
- const repo = await GlobalRepository.create();
486
- const agentService = new AgentService(repo);
487
- const routingService = await RoutingService.create();
488
- const docdex = new DocdexClient({
489
- workspaceRoot: workspace.workspaceRoot,
490
- baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
491
- });
492
- const workspaceRepo = await WorkspaceRepository.create(workspace.workspaceRoot);
493
- const jobService = new JobService(workspace, workspaceRepo);
494
- const selectionService = new TaskSelectionService(workspace, workspaceRepo);
495
- const stateService = new TaskStateService(workspaceRepo);
496
- const vcsClient = new VcsClient();
497
- return new WorkOnTasksService(workspace, {
498
- agentService,
499
- docdex,
500
- jobService,
501
- workspaceRepo,
502
- selectionService,
503
- stateService,
504
- repo,
505
- vcsClient,
506
- routingService,
507
- });
508
- }
509
-
510
- async close(): Promise<void> {
511
- const maybeClose = async (target: unknown) => {
512
- try {
513
- if ((target as any)?.close) await (target as any).close();
514
- } catch {
515
- /* ignore */
516
- }
517
- };
518
- await maybeClose(this.deps.selectionService);
519
- await maybeClose(this.deps.stateService);
520
- await maybeClose(this.deps.agentService);
521
- await maybeClose(this.deps.jobService);
522
- await maybeClose(this.deps.repo);
523
- await maybeClose(this.deps.workspaceRepo);
524
- await maybeClose(this.deps.routingService);
525
- await maybeClose(this.deps.docdex);
526
- }
527
-
528
- private async resolveAgent(agentName?: string) {
529
- const resolved = await this.routingService.resolveAgentForCommand({
530
- workspace: this.workspace,
531
- commandName: "work-on-tasks",
532
- overrideAgentSlug: agentName,
533
- });
534
- return resolved.agent;
535
- }
536
-
537
- private nextLogSeq(taskRunId: string): number {
538
- const next = (this.taskLogSeq.get(taskRunId) ?? 0) + 1;
539
- this.taskLogSeq.set(taskRunId, next);
540
- return next;
541
- }
542
-
543
- private async logTask(taskRunId: string, message: string, source?: string, details?: Record<string, unknown>): Promise<void> {
544
- await this.deps.workspaceRepo.insertTaskLog({
545
- taskRunId,
546
- sequence: this.nextLogSeq(taskRunId),
547
- timestamp: new Date().toISOString(),
548
- source: source ?? "work-on-tasks",
549
- message,
550
- details: details ?? undefined,
551
- });
552
- }
553
-
554
- private async recordTokenUsage(params: {
555
- agentId: string;
556
- model?: string;
557
- jobId: string;
558
- commandRunId: string;
559
- taskRunId: string;
560
- taskId: string;
561
- projectId?: string;
562
- tokensPrompt: number;
563
- tokensCompletion: number;
564
- phase?: string;
565
- durationSeconds?: number;
566
- }) {
567
- const total = params.tokensPrompt + params.tokensCompletion;
568
- await this.deps.jobService.recordTokenUsage({
569
- workspaceId: this.workspace.workspaceId,
570
- agentId: params.agentId,
571
- modelName: params.model,
572
- jobId: params.jobId,
573
- commandRunId: params.commandRunId,
574
- taskRunId: params.taskRunId,
575
- taskId: params.taskId,
576
- projectId: params.projectId,
577
- tokensPrompt: params.tokensPrompt,
578
- tokensCompletion: params.tokensCompletion,
579
- tokensTotal: total,
580
- durationSeconds: params.durationSeconds ?? null,
581
- timestamp: new Date().toISOString(),
582
- metadata: { commandName: "work-on-tasks", phase: params.phase ?? "agent", action: params.phase ?? "agent" },
583
- });
584
- }
585
-
586
- private async updateTaskPhase(
587
- jobId: string,
588
- taskRunId: string,
589
- taskKey: string,
590
- phase: TaskPhase,
591
- status: "start" | "end" | "error",
592
- details?: Record<string, unknown>,
593
- ) {
594
- const payload = { taskKey, phase, status, ...(details ?? {}) };
595
- await this.deps.workspaceRepo.updateTaskRun(taskRunId, { runContext: { phase, status } });
596
- await this.logTask(taskRunId, `${phase}:${status}`, phase, payload);
597
- await this.checkpoint(jobId, `task:${taskKey}:${phase}:${status}`, payload);
598
- }
599
-
600
- private async gatherDocContext(projectKey?: string, docLinks: string[] = []): Promise<{ summary: string; warnings: string[] }> {
601
- const warnings: string[] = [];
602
- const parts: string[] = [];
603
- try {
604
- const docs = await this.deps.docdex.search({ projectKey, profile: "workspace-code" });
605
- parts.push(
606
- ...docs
607
- .slice(0, 5)
608
- .map((doc) => `- [${doc.docType}] ${doc.title ?? doc.path ?? doc.id}`),
609
- );
610
- } catch (error) {
611
- warnings.push(`docdex search failed: ${(error as Error).message}`);
612
- }
613
- for (const link of docLinks) {
614
- try {
615
- const doc = await this.deps.docdex.fetchDocumentById(link);
616
- const excerpt = doc.segments?.[0]?.content?.slice(0, 240);
617
- parts.push(`- [linked:${doc.docType}] ${doc.title ?? doc.id}${excerpt ? ` — ${excerpt}` : ""}`);
618
- } catch (error) {
619
- warnings.push(`docdex fetch failed for ${link}: ${(error as Error).message}`);
620
- }
621
- }
622
- const summary = parts.join("\n");
623
- return { summary, warnings };
624
- }
625
-
626
- private buildPrompt(task: TaskSelectionPlan["ordered"][number], docSummary: string, fileScope: string[]): string {
627
- const deps = task.dependencies.keys.length ? `Depends on: ${task.dependencies.keys.join(", ")}` : "No open dependencies.";
628
- const acceptance = (task.task.acceptanceCriteria ?? []).join("; ");
629
- const docdexHint =
630
- docSummary ||
631
- "Use docdex: search workspace docs with project key and fetch linked documents when present (doc_links metadata).";
632
- return [
633
- `Task ${task.task.key}: ${task.task.title}`,
634
- `Description: ${task.task.description ?? "(none)"}`,
635
- `Epic: ${task.task.epicKey} (${task.task.epicTitle ?? "n/a"}), Story: ${task.task.storyKey} (${task.task.storyTitle ?? "n/a"})`,
636
- `Acceptance: ${acceptance || "Refer to SDS/OpenAPI for expected behavior."}`,
637
- deps,
638
- `Allowed files: ${fileScope.length ? fileScope.join(", ") : "(not constrained)"}`,
639
- `Doc context:\n${docdexHint}`,
640
- "Verify target paths against the current workspace (use docdex/file hints); do not assume hashed or generated asset names exist. If a path is missing, emit a new-file diff with full content (and parent dirs) instead of editing a non-existent file so git apply succeeds. Use JSON.parse-friendly unified diffs.",
641
- "Produce a concise plan and a patch in unified diff fenced with ```patch```.",
642
- ].join("\n");
643
- }
644
-
645
- private async checkoutBaseBranch(baseBranch: string): Promise<void> {
646
- await this.vcs.ensureRepo(this.workspace.workspaceRoot);
647
- await this.vcs.ensureBaseBranch(this.workspace.workspaceRoot, baseBranch);
648
- const dirtyBefore = await this.vcs.dirtyPaths(this.workspace.workspaceRoot);
649
- const nonMcodaBefore = dirtyBefore.filter((p: string) => !p.startsWith(".mcoda"));
650
- if (nonMcodaBefore.length) {
651
- await this.vcs.stage(this.workspace.workspaceRoot, nonMcodaBefore);
652
- const status = await this.vcs.status(this.workspace.workspaceRoot);
653
- if (status.trim().length) {
654
- await this.vcs.commit(this.workspace.workspaceRoot, "[mcoda] auto-commit workspace changes");
655
- }
656
- }
657
- const dirtyAfter = await this.vcs.dirtyPaths(this.workspace.workspaceRoot);
658
- const nonMcodaAfter = dirtyAfter.filter((p: string) => !p.startsWith(".mcoda"));
659
- if (nonMcodaAfter.length) {
660
- throw new Error(`Working tree dirty: ${nonMcodaAfter.join(", ")}`);
661
- }
662
- await this.vcs.checkoutBranch(this.workspace.workspaceRoot, baseBranch);
663
- }
664
-
665
- private async commitPendingChanges(
666
- branchInfo: { branch: string; base: string } | null,
667
- taskKey: string,
668
- taskTitle: string,
669
- reason: string,
670
- taskId: string,
671
- taskRunId: string,
672
- ): Promise<void> {
673
- const dirty = await this.vcs.dirtyPaths(this.workspace.workspaceRoot);
674
- const nonMcoda = dirty.filter((p: string) => !p.startsWith(".mcoda"));
675
- if (!nonMcoda.length) return;
676
- await this.vcs.stage(this.workspace.workspaceRoot, nonMcoda);
677
- const status = await this.vcs.status(this.workspace.workspaceRoot);
678
- if (!status.trim().length) return;
679
- const message = `[${taskKey}] ${taskTitle} (${reason})`;
680
- await this.vcs.commit(this.workspace.workspaceRoot, message);
681
- const head = await this.vcs.lastCommitSha(this.workspace.workspaceRoot);
682
- await this.deps.workspaceRepo.updateTask(taskId, {
683
- vcsLastCommitSha: head,
684
- vcsBranch: branchInfo?.branch ?? null,
685
- vcsBaseBranch: branchInfo?.base ?? null,
686
- });
687
- await this.logTask(taskRunId, `Auto-committed pending changes (${reason})`, "vcs", {
688
- branch: branchInfo?.branch,
689
- base: branchInfo?.base,
690
- head,
691
- });
692
- }
693
-
694
- private async ensureBranches(
695
- taskKey: string,
696
- baseBranch: string,
697
- taskRunId: string,
698
- ): Promise<{ branch: string; base: string; mergeConflicts?: string[]; remoteSyncNote?: string }> {
699
- const branch = `${DEFAULT_TASK_BRANCH_PREFIX}${taskKey}`;
700
- await this.checkoutBaseBranch(baseBranch);
701
- const hasRemote = await this.vcs.hasRemote(this.workspace.workspaceRoot);
702
- if (hasRemote) {
703
- try {
704
- await this.vcs.pull(this.workspace.workspaceRoot, "origin", baseBranch, true);
705
- } catch (error) {
706
- await this.logTask(taskRunId, `Warning: failed to pull ${baseBranch} from origin; continuing with local base.`, "vcs", {
707
- error: (error as Error).message,
708
- });
709
- }
710
- }
711
- const branchExists = await this.vcs.branchExists(this.workspace.workspaceRoot, branch);
712
- let remoteSyncNote = "";
713
- if (branchExists) {
714
- await this.vcs.checkoutBranch(this.workspace.workspaceRoot, branch);
715
- const dirty = (await this.vcs.dirtyPaths(this.workspace.workspaceRoot)).filter((p) => !p.startsWith(".mcoda"));
716
- if (dirty.length) {
717
- throw new Error(`Task branch ${branch} has uncommitted changes: ${dirty.join(", ")}`);
718
- }
719
- if (hasRemote) {
720
- try {
721
- await this.vcs.pull(this.workspace.workspaceRoot, "origin", branch, true);
722
- } catch (error) {
723
- const errorText = this.formatGitError(error);
724
- await this.logTask(taskRunId, `Warning: failed to pull ${branch} from origin; continuing with local branch.`, "vcs", {
725
- error: errorText,
726
- });
727
- if (this.isNonFastForwardPull(errorText)) {
728
- remoteSyncNote = `Remote task branch ${branch} is ahead/diverged. Sync it with origin (pull/rebase or merge) and resolve conflicts before continuing task work.`;
729
- }
730
- }
731
- }
732
- try {
733
- await this.vcs.merge(this.workspace.workspaceRoot, baseBranch, branch, true);
734
- } catch (error) {
735
- const conflicts = await this.vcs.conflictPaths(this.workspace.workspaceRoot);
736
- if (conflicts.length) {
737
- await this.logTask(taskRunId, `Merge conflicts detected while merging ${baseBranch} into ${branch}.`, "vcs", {
738
- conflicts,
739
- });
740
- return { branch, base: baseBranch, mergeConflicts: conflicts, remoteSyncNote };
741
- }
742
- throw new Error(`Failed to merge ${baseBranch} into ${branch}: ${(error as Error).message}`);
743
- }
744
- } else {
745
- await this.vcs.createOrCheckoutBranch(this.workspace.workspaceRoot, branch, baseBranch);
746
- }
747
- return { branch, base: baseBranch, remoteSyncNote: remoteSyncNote || undefined };
748
- }
749
-
750
- private formatGitError(error: unknown): string {
751
- if (!error) return "";
752
- const stderr = typeof (error as any).stderr === "string" ? (error as any).stderr : "";
753
- const stdout = typeof (error as any).stdout === "string" ? (error as any).stdout : "";
754
- const message = error instanceof Error ? error.message : String(error);
755
- return [message, stderr, stdout].filter(Boolean).join(" ");
756
- }
757
-
758
- private isNonFastForwardPush(errorText: string): boolean {
759
- return /non-fast-forward|fetch first|rejected/i.test(errorText);
760
- }
761
-
762
- private isNonFastForwardPull(errorText: string): boolean {
763
- return /not possible to fast-forward|divergent|non-fast-forward|rejected/i.test(errorText);
764
- }
765
-
766
- private isRemotePermissionError(errorText: string): boolean {
767
- return /protected branch|gh006|permission denied|not authorized|not allowed to push|access denied|403|forbidden/i.test(errorText);
768
- }
769
-
770
- private isCommitHookFailure(errorText: string): boolean {
771
- return /hook|pre-commit|commit-msg|husky/i.test(errorText);
772
- }
773
-
774
- private isGpgSignFailure(errorText: string): boolean {
775
- return /gpg|signing key|signing failed|gpg failed|no secret key/i.test(errorText);
776
- }
777
- private async pushWithRecovery(taskRunId: string, branch: string): Promise<{ pushed: boolean; skipped: boolean; reason?: string }> {
778
- const cwd = this.workspace.workspaceRoot;
779
- try {
780
- await this.vcs.push(cwd, "origin", branch);
781
- return { pushed: true, skipped: false };
782
- } catch (error) {
783
- const errorText = this.formatGitError(error);
784
- if (this.isRemotePermissionError(errorText)) {
785
- await this.logTask(
786
- taskRunId,
787
- `Remote rejected push for ${branch} due to permissions or branch protection; continuing with local commits.`,
788
- "vcs",
789
- {
790
- error: errorText,
791
- guidance: "Use a token with write access or push to an unprotected branch and open a PR.",
792
- },
793
- );
794
- return { pushed: false, skipped: true, reason: "permission" };
795
- }
796
- if (!this.isNonFastForwardPush(errorText)) {
797
- throw error;
798
- }
799
- await this.logTask(
800
- taskRunId,
801
- `Non-fast-forward push rejected for ${branch}; attempting to pull and retry.`,
802
- "vcs",
803
- { error: errorText },
804
- );
805
- const currentBranch = await this.vcs.currentBranch(cwd);
806
- if (currentBranch && currentBranch !== branch) {
807
- await this.vcs.ensureClean(cwd);
808
- await this.vcs.checkoutBranch(cwd, branch);
809
- }
810
- try {
811
- await this.vcs.pull(cwd, "origin", branch, false);
812
- await this.vcs.push(cwd, "origin", branch);
813
- } catch (retryError) {
814
- const retryText = this.formatGitError(retryError);
815
- if (this.isRemotePermissionError(retryText)) {
816
- await this.logTask(
817
- taskRunId,
818
- `Remote rejected push for ${branch} after sync due to permissions or branch protection; continuing with local commits.`,
819
- "vcs",
820
- {
821
- error: retryText,
822
- guidance: "Use a token with write access or push to an unprotected branch and open a PR.",
823
- },
824
- );
825
- return { pushed: false, skipped: true, reason: "permission" };
826
- }
827
- throw new Error(`Non-fast-forward push rejected for ${branch}; retry after pull failed: ${retryText}`);
828
- } finally {
829
- if (currentBranch && currentBranch !== branch) {
830
- await this.vcs.checkoutBranch(cwd, currentBranch);
831
- }
832
- }
833
- await this.logTask(taskRunId, `Push recovered after syncing ${branch} from origin.`, "vcs");
834
- return { pushed: true, skipped: false };
835
- }
836
- }
837
-
838
- private validateScope(allowed: string[], touched: string[]): { ok: boolean; message?: string } {
839
- if (!allowed.length) return { ok: true };
840
- const normalizedAllowed = allowed.map((f) => f.replace(/\\/g, "/"));
841
- const outOfScope = touched.filter((f) => !normalizedAllowed.some((allowedPath) => f === allowedPath || f.startsWith(`${allowedPath}/`)));
842
- if (outOfScope.length) {
843
- return { ok: false, message: `Patch touches files outside allowed scope: ${outOfScope.join(", ")}` };
844
- }
845
- return { ok: true };
846
- }
847
-
848
- private async applyPatches(
849
- patches: string[],
850
- cwd: string,
851
- dryRun: boolean,
852
- ): Promise<{ touched: string[]; error?: string; warnings?: string[] }> {
853
- const touched = new Set<string>();
854
- const warnings: string[] = [];
855
- let applied = 0;
856
- for (const patch of patches) {
857
- const normalized = maybeConvertApplyPatch(patch);
858
- const withHeader = ensureDiffHeader(normalized);
859
- const withHunks = normalizeHunkHeaders(withHeader);
860
- const withPrefixes = fixMissingPrefixesInHunks(withHunks);
861
- const sanitized = stripInvalidIndexLines(withPrefixes);
862
- if (isPlaceholderPatch(sanitized)) {
863
- warnings.push("Skipped placeholder patch that contained ??? or 'rest of existing code'.");
864
- continue;
865
- }
866
- const files = touchedFilesFromPatch(sanitized);
867
- if (!files.length) {
868
- warnings.push("Skipped patch with no recognizable file paths.");
869
- continue;
870
- }
871
- const segments = splitPatchIntoDiffs(sanitized);
872
- for (const segment of segments) {
873
- const segmentFiles = touchedFilesFromPatch(segment);
874
- const existingFiles = new Set(segmentFiles.map((f) => path.join(cwd, f)).filter((f) => fs.existsSync(f)));
875
- let patchToApply = segment;
876
- if (existingFiles.size > 0) {
877
- const { patch: converted, skipped } = updateAddPatchForExistingFile(segment, existingFiles, cwd);
878
- patchToApply = converted;
879
- if (skipped.length) {
880
- warnings.push(`Skipped add patch for existing files: ${skipped.join(", ")}`);
881
- continue;
882
- }
883
- }
884
- if (dryRun) {
885
- segmentFiles.forEach((f) => touched.add(f));
886
- applied += 1;
887
- continue;
888
- }
889
- // Ensure target directories exist for new/updated files.
890
- for (const file of segmentFiles) {
891
- const dir = path.dirname(path.join(cwd, file));
892
- try {
893
- await fs.promises.mkdir(dir, { recursive: true });
894
- } catch {
895
- /* ignore mkdir errors; git apply will surface issues */
896
- }
897
- }
898
- try {
899
- await this.vcs.applyPatch(cwd, patchToApply);
900
- segmentFiles.forEach((f) => touched.add(f));
901
- applied += 1;
902
- } catch (error) {
903
- // Fallback: if the segment only adds new files and git apply fails, write the files directly.
904
- const additions = parseAddedFileContents(patchToApply);
905
- const addTargets = Object.keys(additions);
906
- if (addTargets.length && segmentFiles.length === addTargets.length) {
907
- try {
908
- for (const file of addTargets) {
909
- const dest = path.join(cwd, file);
910
- await fs.promises.mkdir(path.dirname(dest), { recursive: true });
911
- await fs.promises.writeFile(dest, additions[file], "utf8");
912
- touched.add(file);
913
- }
914
- applied += 1;
915
- warnings.push(`Applied add-only segment by writing files directly: ${addTargets.join(", ")}`);
916
- continue;
917
- } catch (writeError) {
918
- warnings.push(
919
- `Patch segment failed and fallback write failed (${segmentFiles.join(", ") || "unknown files"}): ${(writeError as Error).message}`,
920
- );
921
- continue;
922
- }
923
- }
924
- warnings.push(`Patch segment failed (${segmentFiles.join(", ") || "unknown files"}): ${(error as Error).message}`);
925
- }
926
- }
927
- }
928
- if (!applied && warnings.length) {
929
- return { touched: Array.from(touched), warnings, error: "No patches applied; all were skipped as placeholders." };
930
- }
931
- return { touched: Array.from(touched), warnings };
932
- }
933
-
934
- private async applyFileBlocks(
935
- files: Array<{ path: string; content: string }>,
936
- cwd: string,
937
- dryRun: boolean,
938
- allowNoop = false,
939
- ): Promise<{ touched: string[]; error?: string; warnings?: string[]; appliedCount: number }> {
940
- const touched = new Set<string>();
941
- const warnings: string[] = [];
942
- let applied = 0;
943
- for (const file of files) {
944
- const relative = file.path.trim();
945
- if (!relative) {
946
- warnings.push("Skipped file block with empty path.");
947
- continue;
948
- }
949
- const resolved = path.resolve(cwd, relative);
950
- const relativePath = path.relative(cwd, resolved).replace(/\\/g, "/");
951
- if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
952
- warnings.push(`Skipped file block outside workspace: ${relative}`);
953
- continue;
954
- }
955
- if (fs.existsSync(resolved)) {
956
- warnings.push(`Skipped file block for existing file: ${relativePath}`);
957
- continue;
958
- }
959
- if (dryRun) {
960
- touched.add(relativePath);
961
- applied += 1;
962
- continue;
963
- }
964
- try {
965
- await fs.promises.mkdir(path.dirname(resolved), { recursive: true });
966
- await fs.promises.writeFile(resolved, file.content ?? "", "utf8");
967
- touched.add(relativePath);
968
- applied += 1;
969
- } catch (error) {
970
- warnings.push(`Failed to write file block ${relativePath}: ${(error as Error).message}`);
971
- }
972
- }
973
- if (!applied && !allowNoop) {
974
- return {
975
- touched: Array.from(touched),
976
- warnings,
977
- error: "No file blocks were applied.",
978
- appliedCount: applied,
979
- };
980
- }
981
- return { touched: Array.from(touched), warnings, appliedCount: applied };
982
- }
983
-
984
- private async runTests(commands: string[], cwd: string): Promise<{ ok: boolean; results: { command: string; stdout: string; stderr: string; code: number }[] }> {
985
- const results: { command: string; stdout: string; stderr: string; code: number }[] = [];
986
- for (const command of commands) {
987
- try {
988
- const { stdout, stderr } = await exec(command, { cwd });
989
- results.push({ command, stdout, stderr, code: 0 });
990
- } catch (error: any) {
991
- results.push({
992
- command,
993
- stdout: error.stdout ?? "",
994
- stderr: error.stderr ?? String(error),
995
- code: typeof error.code === "number" ? error.code : 1,
996
- });
997
- return { ok: false, results };
998
- }
999
- }
1000
- return { ok: true, results };
1001
- }
1002
-
1003
- async workOnTasks(request: WorkOnTasksRequest): Promise<WorkOnTasksResult> {
1004
- await this.ensureMcoda();
1005
- const agentStream = request.agentStream !== false;
1006
- const configuredBaseBranch = request.baseBranch ?? this.workspace.config?.branch;
1007
- const baseBranch = DEFAULT_BASE_BRANCH;
1008
- const baseBranchWarnings =
1009
- configuredBaseBranch && configuredBaseBranch !== baseBranch
1010
- ? [`Base branch forced to ${baseBranch}; ignoring configured ${configuredBaseBranch}.`]
1011
- : [];
1012
- const commandRun = await this.deps.jobService.startCommandRun("work-on-tasks", request.projectKey, {
1013
- taskIds: request.taskKeys,
1014
- });
1015
- const job = await this.deps.jobService.startJob("work", commandRun.id, request.projectKey, {
1016
- commandName: "work-on-tasks",
1017
- payload: {
1018
- projectKey: request.projectKey,
1019
- epicKey: request.epicKey,
1020
- storyKey: request.storyKey,
1021
- tasks: request.taskKeys,
1022
- statusFilter: request.statusFilter,
1023
- limit: request.limit,
1024
- parallel: request.parallel,
1025
- noCommit: request.noCommit ?? false,
1026
- dryRun: request.dryRun ?? false,
1027
- agent: request.agentName,
1028
- agentStream,
1029
- },
1030
- });
1031
-
1032
- let selection: TaskSelectionPlan;
1033
- let storyPointsProcessed = 0;
1034
- try {
1035
- await this.checkoutBaseBranch(baseBranch);
1036
- selection = await this.selectionService.selectTasks({
1037
- projectKey: request.projectKey,
1038
- epicKey: request.epicKey,
1039
- storyKey: request.storyKey,
1040
- taskKeys: request.taskKeys,
1041
- statusFilter: request.statusFilter,
1042
- limit: request.limit,
1043
- parallel: request.parallel,
1044
- });
1045
-
1046
- await this.checkpoint(job.id, "selection", {
1047
- ordered: selection.ordered.map((t) => t.task.key),
1048
- blocked: selection.blocked.map((t) => t.task.key),
1049
- });
1050
-
1051
- await this.deps.jobService.updateJobStatus(job.id, "running", {
1052
- payload: {
1053
- ...(job.payload ?? {}),
1054
- selection: selection.ordered.map((t) => t.task.key),
1055
- blocked: selection.blocked.map((t) => t.task.key),
1056
- },
1057
- totalItems: selection.ordered.length,
1058
- processedItems: 0,
1059
- });
1060
-
1061
- const results: TaskExecutionResult[] = [];
1062
- const warnings: string[] = [...baseBranchWarnings, ...selection.warnings];
1063
- const agent = await this.resolveAgent(request.agentName);
1064
- const prompts = await this.loadPrompts(agent.id);
1065
- const formatSessionId = (iso: string): string => {
1066
- const date = new Date(iso);
1067
- const pad = (value: number) => String(value).padStart(2, "0");
1068
- return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(
1069
- date.getMinutes(),
1070
- )}${pad(date.getSeconds())}`;
1071
- };
1072
- const formatDuration = (ms: number): string => {
1073
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
1074
- const seconds = totalSeconds % 60;
1075
- const minutesTotal = Math.floor(totalSeconds / 60);
1076
- const minutes = minutesTotal % 60;
1077
- const hours = Math.floor(minutesTotal / 60);
1078
- if (hours > 0) return `${hours}H ${minutes}M ${seconds}S`;
1079
- return `${minutes}M ${seconds}S`;
1080
- };
1081
- const formatCount = (value: number): string => value.toLocaleString("en-US");
1082
- const emitLine = (line: string): void => {
1083
- if (request.onAgentChunk) {
1084
- request.onAgentChunk(`${line}\n`);
1085
- return;
1086
- }
1087
- console.info(line);
1088
- };
1089
- const emitBlank = (): void => emitLine("");
1090
- const resolveProvider = (adapter?: string): string => {
1091
- if (!adapter) return "n/a";
1092
- const trimmed = adapter.trim();
1093
- if (!trimmed) return "n/a";
1094
- if (trimmed.includes("-")) return trimmed.split("-")[0];
1095
- return trimmed;
1096
- };
1097
- const resolveReasoning = (config?: Record<string, unknown>): string => {
1098
- if (!config) return "n/a";
1099
- const raw = (config as Record<string, unknown>).reasoning ?? (config as Record<string, unknown>).thinking;
1100
- if (typeof raw === "string") return raw;
1101
- if (typeof raw === "boolean") return raw ? "enabled" : "disabled";
1102
- return "n/a";
1103
- };
1104
- const emitTaskStart = (details: {
1105
- taskKey: string;
1106
- alias: string;
1107
- summary: string;
1108
- model: string;
1109
- provider: string;
1110
- step: string;
1111
- reasoning: string;
1112
- workdir: string;
1113
- sessionId: string;
1114
- }): void => {
1115
- emitLine("╭──────────────────────────────────────────────────────────╮");
1116
- emitLine("│ START OF TASK │");
1117
- emitLine("╰──────────────────────────────────────────────────────────╯");
1118
- emitLine(` [🪪] Start Task ID: ${details.taskKey}`);
1119
- emitLine(` [👹] Alias: ${details.alias}`);
1120
- emitLine(` [ℹ️] Summary: ${details.summary}`);
1121
- emitLine(` [🤖] Model: ${details.model}`);
1122
- emitLine(` [🕹️] Provider: ${details.provider}`);
1123
- emitLine(` [🧩] Step: ${details.step}`);
1124
- emitLine(` [🧠] Reasoning: ${details.reasoning}`);
1125
- emitLine(` [📁] Workdir: ${details.workdir}`);
1126
- emitLine(` [🔑] Session: ${details.sessionId}`);
1127
- emitBlank();
1128
- emitLine(" ░░░░░ START OF A NEW TASK ░░░░░");
1129
- emitBlank();
1130
- emitLine(` [STEP ${details.step}] [MODEL ${details.model}]`);
1131
- emitBlank();
1132
- emitBlank();
1133
- };
1134
- const emitTaskEnd = async (details: {
1135
- taskKey: string;
1136
- status: TaskExecutionResult["status"];
1137
- terminal: string;
1138
- storyPoints?: number | null;
1139
- elapsedMs: number;
1140
- tokensPrompt: number;
1141
- tokensCompletion: number;
1142
- promptEstimate: number;
1143
- taskBranch?: string | null;
1144
- baseBranch?: string | null;
1145
- touchedFiles: number;
1146
- mergeStatus: "merged" | "skipped" | "failed";
1147
- headSha?: string | null;
1148
- }): Promise<void> => {
1149
- const tokensTotal = details.tokensPrompt + details.tokensCompletion;
1150
- const promptEstimate = Math.max(1, details.promptEstimate);
1151
- const usagePercent = (tokensTotal / promptEstimate) * 100;
1152
- const completion = details.status === "succeeded" ? 100 : 0;
1153
- const completionBar = "💰".repeat(15);
1154
- const statusLabel =
1155
- details.status === "succeeded"
1156
- ? "COMPLETED"
1157
- : details.status === "skipped"
1158
- ? "SKIPPED"
1159
- : details.status === "blocked"
1160
- ? "BLOCKED"
1161
- : "FAILED";
1162
- const hasRemote = await this.vcs.hasRemote(this.workspace.workspaceRoot);
1163
- const tracking = details.taskBranch ? (hasRemote ? `origin/${details.taskBranch}` : "n/a") : "n/a";
1164
- const headSha = details.headSha ?? "n/a";
1165
- const baseLabel = details.baseBranch ?? baseBranch;
1166
- emitLine("╭──────────────────────────────────────────────────────────╮");
1167
- emitLine("│ END OF TASK │");
1168
- emitLine("╰──────────────────────────────────────────────────────────╯");
1169
- emitLine(
1170
- ` 👏🏼 TASK ${details.taskKey} | 📜 STATUS ${statusLabel} | 🏠 TERMINAL ${details.terminal} | ⚡ SP ${
1171
- details.storyPoints ?? 0
1172
- } | ⌛ TIME ${formatDuration(details.elapsedMs)}`,
1173
- );
1174
- emitBlank();
1175
- emitLine(` [${completionBar}] ${completion.toFixed(1)}% Complete`);
1176
- emitLine(` Tokens used: ${formatCount(tokensTotal)}`);
1177
- emitLine(` ${usagePercent.toFixed(1)}% used vs prompt est (x${(tokensTotal / promptEstimate).toFixed(2)})`);
1178
- emitLine(` Est. tokens: ${formatCount(promptEstimate)}`);
1179
- emitBlank();
1180
- emitLine("🌿 Git summary");
1181
- emitLine("────────────────────────────────────────────────────────────");
1182
- emitLine(` [🎋] Task branch: ${details.taskBranch ?? "n/a"}`);
1183
- emitLine(` [🗿] Tracking: ${tracking}`);
1184
- emitLine(` [🚀] Merge→dev: ${details.mergeStatus}`);
1185
- emitLine(` [🐲] HEAD: ${headSha}`);
1186
- emitLine(` [♨️] Files: ${details.touchedFiles}`);
1187
- emitLine(` [🔑] Base: ${baseLabel}`);
1188
- emitLine(" [🧾] Git log: n/a");
1189
- emitBlank();
1190
- emitLine("🗂 Artifacts");
1191
- emitLine("────────────────────────────────────────────────────────────");
1192
- emitLine(" • History: n/a");
1193
- emitLine(" • Git log: n/a");
1194
- emitBlank();
1195
- emitLine(" ░░░░░ END OF THE TASK WORK ░░░░░");
1196
- emitBlank();
1197
- };
1198
-
1199
- for (const [index, task] of selection.ordered.entries()) {
1200
- const startedAt = new Date().toISOString();
1201
- const taskRun = await this.deps.workspaceRepo.createTaskRun({
1202
- taskId: task.task.id,
1203
- command: "work-on-tasks",
1204
- jobId: job.id,
1205
- commandRunId: commandRun.id,
1206
- agentId: agent.id,
1207
- status: "running",
1208
- startedAt,
1209
- storyPointsAtRun: task.task.storyPoints ?? null,
1210
- gitBranch: task.task.vcsBranch ?? null,
1211
- gitBaseBranch: task.task.vcsBaseBranch ?? null,
1212
- gitCommitSha: task.task.vcsLastCommitSha ?? null,
1213
- });
1214
-
1215
- const sessionId = formatSessionId(startedAt);
1216
- const taskAlias = `Working on task ${task.task.key}`;
1217
- const taskSummary = task.task.title || task.task.description || "(none)";
1218
- const modelLabel = agent.defaultModel ?? "(default)";
1219
- const providerLabel = resolveProvider(agent.adapter);
1220
- const reasoningLabel = resolveReasoning(agent.config as Record<string, unknown> | undefined);
1221
- const stepLabel = "patch";
1222
- const taskStartMs = Date.now();
1223
- let taskStatus: TaskExecutionResult["status"] | null = null;
1224
- let tokensPromptTotal = 0;
1225
- let tokensCompletionTotal = 0;
1226
- let promptEstimateBase = 0;
1227
- let promptEstimateTotal = 0;
1228
- let mergeStatus: "merged" | "skipped" | "failed" = "skipped";
1229
- let patchApplied = false;
1230
- let touched: string[] = [];
1231
- let taskBranchName: string | null = task.task.vcsBranch ?? null;
1232
- let baseBranchName: string | null = task.task.vcsBaseBranch ?? baseBranch;
1233
- let branchInfo: { branch: string; base: string; mergeConflicts?: string[]; remoteSyncNote?: string } | null = {
1234
- branch: task.task.vcsBranch ?? "",
1235
- base: task.task.vcsBaseBranch ?? baseBranch,
1236
- };
1237
- let headSha: string | null = task.task.vcsLastCommitSha ?? null;
1238
- let taskEndEmitted = false;
1239
-
1240
- emitTaskStart({
1241
- taskKey: task.task.key,
1242
- alias: taskAlias,
1243
- summary: taskSummary,
1244
- model: modelLabel,
1245
- provider: providerLabel,
1246
- step: stepLabel,
1247
- reasoning: reasoningLabel,
1248
- workdir: this.workspace.workspaceRoot,
1249
- sessionId,
1250
- });
1251
-
1252
- const emitTaskEndOnce = async () => {
1253
- if (taskEndEmitted) return;
1254
- taskEndEmitted = true;
1255
- const status = taskStatus ?? "failed";
1256
- const terminal =
1257
- status === "succeeded"
1258
- ? touched.length
1259
- ? "COMPLETED_WITH_CHANGES"
1260
- : "COMPLETED_NO_CHANGES"
1261
- : status === "blocked"
1262
- ? "BLOCKED"
1263
- : status === "skipped"
1264
- ? "SKIPPED"
1265
- : "FAILED";
1266
- let resolvedHead = headSha;
1267
- if (!resolvedHead) {
1268
- try {
1269
- resolvedHead = await this.vcs.lastCommitSha(this.workspace.workspaceRoot);
1270
- } catch {
1271
- resolvedHead = null;
1272
- }
1273
- }
1274
- await emitTaskEnd({
1275
- taskKey: task.task.key,
1276
- status,
1277
- terminal,
1278
- storyPoints: task.task.storyPoints ?? 0,
1279
- elapsedMs: Date.now() - taskStartMs,
1280
- tokensPrompt: tokensPromptTotal,
1281
- tokensCompletion: tokensCompletionTotal,
1282
- promptEstimate: promptEstimateTotal || promptEstimateBase,
1283
- taskBranch: taskBranchName || null,
1284
- baseBranch: baseBranchName || baseBranch,
1285
- touchedFiles: touched.length,
1286
- mergeStatus,
1287
- headSha: resolvedHead,
1288
- });
1289
- };
1290
-
1291
- const phaseTimers: Partial<Record<TaskPhase, number>> = {};
1292
- const startPhase = async (phase: TaskPhase, details?: Record<string, unknown>) => {
1293
- phaseTimers[phase] = Date.now();
1294
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, phase, "start", details);
1295
- };
1296
- const endPhase = async (phase: TaskPhase, details?: Record<string, unknown>) => {
1297
- const started = phaseTimers[phase];
1298
- const durationSeconds = started ? Math.round(((Date.now() - started) / 1000) * 1000) / 1000 : undefined;
1299
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, phase, "end", {
1300
- ...(details ?? {}),
1301
- durationSeconds,
1302
- });
1303
- };
1304
-
1305
- try {
1306
- await startPhase("selection", {
1307
- dependencies: task.dependencies.keys,
1308
- blockedReason: task.blockedReason,
1309
- });
1310
- await this.logTask(taskRun.id, `Selected task ${task.task.key}`, "selection", {
1311
- dependencies: task.dependencies.keys,
1312
- blockedReason: task.blockedReason,
1313
- });
1314
-
1315
- if (task.blockedReason && !request.dryRun) {
1316
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, "selection", "error", {
1317
- blockedReason: task.blockedReason,
1318
- });
1319
- await this.stateService.markBlocked(task.task, task.blockedReason);
1320
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
1321
- status: "failed",
1322
- finishedAt: new Date().toISOString(),
1323
- });
1324
- results.push({ taskKey: task.task.key, status: "blocked", notes: task.blockedReason });
1325
- taskStatus = "blocked";
1326
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1327
- await emitTaskEndOnce();
1328
- continue;
1329
- }
1330
-
1331
- await endPhase("selection");
1332
- } catch (error) {
1333
- const message = `Selection phase failed: ${(error as Error).message}`;
1334
- try {
1335
- await this.logTask(taskRun.id, message, "selection");
1336
- } catch {
1337
- /* ignore log failures */
1338
- }
1339
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
1340
- status: "failed",
1341
- finishedAt: new Date().toISOString(),
1342
- });
1343
- results.push({ taskKey: task.task.key, status: "failed", notes: message });
1344
- taskStatus = "failed";
1345
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1346
- await emitTaskEndOnce();
1347
- continue;
1348
- }
1349
- let lockAcquired = false;
1350
- if (!request.dryRun) {
1351
- const ttlSeconds = Math.max(1, TASK_LOCK_TTL_SECONDS);
1352
- const lockResult = await this.deps.workspaceRepo.tryAcquireTaskLock(
1353
- task.task.id,
1354
- taskRun.id,
1355
- job.id,
1356
- ttlSeconds,
1357
- );
1358
- if (!lockResult.acquired) {
1359
- await this.logTask(taskRun.id, "Task already locked by another run; skipping.", "vcs", {
1360
- lock: lockResult.lock ?? null,
1361
- });
1362
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
1363
- status: "cancelled",
1364
- finishedAt: new Date().toISOString(),
1365
- });
1366
- results.push({ taskKey: task.task.key, status: "skipped", notes: "task_locked" });
1367
- taskStatus = "skipped";
1368
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1369
- await emitTaskEndOnce();
1370
- continue;
1371
- }
1372
- lockAcquired = true;
1373
- }
1374
-
1375
- try {
1376
- const metadata = (task.task.metadata as any) ?? {};
1377
- let allowedFiles = Array.isArray(metadata.files) ? normalizePaths(this.workspace.workspaceRoot, metadata.files) : [];
1378
- const testCommands = Array.isArray(metadata.tests) ? (metadata.tests as string[]) : [];
1379
- let mergeConflicts: string[] = [];
1380
- let remoteSyncNote = "";
1381
- const softFailures: string[] = [];
1382
- let lastLockRefresh = Date.now();
1383
- const getLockRefreshIntervalMs = () => {
1384
- const ttlSeconds = Math.max(1, TASK_LOCK_TTL_SECONDS);
1385
- const ttlMs = ttlSeconds * 1000;
1386
- return Math.max(250, Math.min(ttlMs - 250, Math.floor(ttlMs / 3)));
1387
- };
1388
- const refreshLock = async (label: string, force = false): Promise<boolean> => {
1389
- if (!lockAcquired) return true;
1390
- const now = Date.now();
1391
- if (!force && now - lastLockRefresh < getLockRefreshIntervalMs()) return true;
1392
- try {
1393
- const ttlSeconds = Math.max(1, TASK_LOCK_TTL_SECONDS);
1394
- const refreshed = await this.deps.workspaceRepo.refreshTaskLock(task.task.id, taskRun.id, ttlSeconds);
1395
- if (!refreshed) {
1396
- await this.logTask(taskRun.id, `Task lock lost during ${label}; another run may have taken it.`, "vcs", {
1397
- reason: "lock_stolen",
1398
- });
1399
- }
1400
- if (refreshed) {
1401
- lastLockRefresh = now;
1402
- }
1403
- return refreshed;
1404
- } catch (error) {
1405
- await this.logTask(taskRun.id, `Failed to refresh task lock (${label}); treating as lock loss.`, "vcs", {
1406
- error: (error as Error).message,
1407
- reason: "refresh_failed",
1408
- });
1409
- return false;
1410
- }
1411
- return true;
1412
- };
1413
-
1414
- if (!request.dryRun) {
1415
- try {
1416
- branchInfo = await this.ensureBranches(task.task.key, baseBranch, taskRun.id);
1417
- taskBranchName = branchInfo.branch || taskBranchName;
1418
- baseBranchName = branchInfo.base || baseBranchName;
1419
- mergeConflicts = branchInfo.mergeConflicts ?? [];
1420
- remoteSyncNote = branchInfo.remoteSyncNote ?? "";
1421
- if (mergeConflicts.length && allowedFiles.length) {
1422
- allowedFiles = Array.from(new Set([...allowedFiles, ...mergeConflicts.map((f) => f.replace(/\\/g, "/"))]));
1423
- }
1424
- await this.deps.workspaceRepo.updateTask(task.task.id, {
1425
- vcsBranch: branchInfo.branch,
1426
- vcsBaseBranch: branchInfo.base,
1427
- });
1428
- await this.logTask(taskRun.id, `Using branch ${branchInfo.branch} (base ${branchInfo.base})`, "vcs");
1429
- } catch (error) {
1430
- const message = `Failed to prepare branches: ${(error as Error).message}`;
1431
- await this.logTask(taskRun.id, message, "vcs");
1432
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1433
- results.push({ taskKey: task.task.key, status: "failed", notes: message });
1434
- taskStatus = "failed";
1435
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1436
- continue;
1437
- }
1438
- }
1439
-
1440
- await startPhase("context", { allowedFiles, tests: testCommands });
1441
- const docLinks = Array.isArray((metadata as any).doc_links) ? (metadata as any).doc_links : [];
1442
- const { summary: docSummary, warnings: docWarnings } = await this.gatherDocContext(request.projectKey, docLinks);
1443
- if (docWarnings.length) {
1444
- warnings.push(...docWarnings);
1445
- await this.logTask(taskRun.id, docWarnings.join("; "), "docdex");
1446
- }
1447
- await endPhase("context", { docWarnings, docSummary: Boolean(docSummary) });
1448
-
1449
- await startPhase("prompt", { docSummary: Boolean(docSummary), agent: agent.id });
1450
- const conflictNote = mergeConflicts.length
1451
- ? `Merge conflicts detected in: ${mergeConflicts.join(", ")}. Resolve these conflicts before any other task work. Remove conflict markers and ensure the files are consistent.`
1452
- : "";
1453
- const promptBase = this.buildPrompt(task, docSummary, allowedFiles);
1454
- const notes = [remoteSyncNote, conflictNote].filter(Boolean).join("\n");
1455
- const prompt = notes ? `${notes}\n\n${promptBase}` : promptBase;
1456
- const commandPrompt = prompts.commandPrompt ?? "";
1457
- const systemPrompt = [prompts.jobPrompt, prompts.characterPrompt, commandPrompt].filter(Boolean).join("\n\n");
1458
- await this.logTask(taskRun.id, `System prompt:\n${systemPrompt || "(none)"}`, "prompt");
1459
- await this.logTask(taskRun.id, `Task prompt:\n${prompt}`, "prompt");
1460
- promptEstimateBase = estimateTokens(systemPrompt + prompt);
1461
- await endPhase("prompt", { hasSystemPrompt: Boolean(systemPrompt) });
1462
-
1463
- if (request.dryRun) {
1464
- await this.logTask(taskRun.id, "Dry-run enabled; skipping execution.", "execution");
1465
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
1466
- status: "cancelled",
1467
- finishedAt: new Date().toISOString(),
1468
- });
1469
- results.push({ taskKey: task.task.key, status: "skipped", notes: "dry_run" });
1470
- taskStatus = "skipped";
1471
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1472
- continue;
1473
- }
1474
-
1475
- try {
1476
- await this.stateService.transitionToInProgress(task.task);
1477
- } catch (error) {
1478
- await this.logTask(taskRun.id, `Failed to move task to in_progress: ${(error as Error).message}`, "state");
1479
- }
1480
-
1481
- const streamChunk = (text?: string) => {
1482
- if (!text) return;
1483
- if (request.onAgentChunk) {
1484
- request.onAgentChunk(text);
1485
- return;
1486
- }
1487
- if (agentStream) {
1488
- process.stdout.write(text);
1489
- }
1490
- };
1491
-
1492
- const invokeAgentOnce = async (input: string, phaseLabel: string) => {
1493
- let output = "";
1494
- const started = Date.now();
1495
- if (agentStream && this.deps.agentService.invokeStream) {
1496
- const stream = await this.deps.agentService.invokeStream(agent.id, {
1497
- input,
1498
- metadata: { taskKey: task.task.key },
1499
- });
1500
- let pollLockLost = false;
1501
- const refreshTimer = setInterval(() => {
1502
- void refreshLock("agent_stream_poll").then((ok) => {
1503
- if (!ok) pollLockLost = true;
1504
- });
1505
- }, getLockRefreshIntervalMs());
1506
- try {
1507
- for await (const chunk of stream) {
1508
- output += chunk.output ?? "";
1509
- streamChunk(chunk.output);
1510
- await this.logTask(taskRun.id, chunk.output ?? "", phaseLabel);
1511
- if (!(await refreshLock("agent_stream"))) {
1512
- await this.logTask(taskRun.id, "Aborting task: lock lost during agent streaming.", "vcs");
1513
- throw new Error("Task lock lost during agent stream.");
1514
- }
1515
- }
1516
- } finally {
1517
- clearInterval(refreshTimer);
1518
- }
1519
- if (pollLockLost) {
1520
- await this.logTask(taskRun.id, "Aborting task: lock lost during agent stream.", "vcs");
1521
- throw new Error("Task lock lost during agent stream.");
1522
- }
1523
- } else {
1524
- let pollLockLost = false;
1525
- let rejectLockLost: ((error: Error) => void) | null = null;
1526
- const lockLostPromise = new Promise<never>((_, reject) => {
1527
- rejectLockLost = reject;
1528
- });
1529
- const refreshTimer = setInterval(() => {
1530
- void refreshLock("agent_poll").then((ok) => {
1531
- if (ok || pollLockLost) return;
1532
- pollLockLost = true;
1533
- if (rejectLockLost) rejectLockLost(new Error("Task lock lost during agent invoke."));
1534
- });
1535
- }, getLockRefreshIntervalMs());
1536
- const invokePromise = this.deps.agentService
1537
- .invoke(agent.id, { input, metadata: { taskKey: task.task.key } })
1538
- .catch((error) => {
1539
- if (pollLockLost) return null as any;
1540
- throw error;
1541
- });
1542
- try {
1543
- const result = await Promise.race([invokePromise, lockLostPromise]);
1544
- if (result) {
1545
- output = result.output ?? "";
1546
- }
1547
- } finally {
1548
- clearInterval(refreshTimer);
1549
- }
1550
- if (pollLockLost) {
1551
- await this.logTask(taskRun.id, "Aborting task: lock lost during agent invoke.", "vcs");
1552
- throw new Error("Task lock lost during agent invoke.");
1553
- }
1554
- streamChunk(output);
1555
- await this.logTask(taskRun.id, output, phaseLabel);
1556
- }
1557
- return { output, durationSeconds: (Date.now() - started) / 1000 };
1558
- };
1559
-
1560
- let agentOutput = "";
1561
- let agentDuration = 0;
1562
- let triedRetry = false;
1563
-
1564
- const agentInput = `${systemPrompt}\n\n${prompt}`;
1565
- try {
1566
- await startPhase("agent", { agent: agent.id, stream: agentStream });
1567
- const first = await invokeAgentOnce(agentInput, "agent");
1568
- agentOutput = first.output;
1569
- agentDuration = first.durationSeconds;
1570
- await endPhase("agent", { agentDurationSeconds: agentDuration });
1571
- if (!(await refreshLock("agent"))) {
1572
- await this.logTask(taskRun.id, "Aborting task: lock lost after agent completion.", "vcs");
1573
- throw new Error("Task lock lost after agent completion.");
1574
- }
1575
- } catch (error) {
1576
- const message = error instanceof Error ? error.message : String(error);
1577
- if (/task lock lost/i.test(message)) {
1578
- throw error;
1579
- }
1580
- await this.logTask(taskRun.id, `Agent invocation failed: ${message}`, "agent");
1581
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, "agent", "error", { error: message });
1582
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
1583
- status: "failed",
1584
- finishedAt: new Date().toISOString(),
1585
- });
1586
- results.push({ taskKey: task.task.key, status: "failed", notes: message });
1587
- taskStatus = "failed";
1588
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1589
- continue;
1590
- }
1591
-
1592
- const recordUsage = async (
1593
- phase: "agent" | "agent_retry",
1594
- output: string,
1595
- durationSeconds: number,
1596
- promptText: string,
1597
- ) => {
1598
- const promptTokens = estimateTokens(promptText);
1599
- const completionTokens = estimateTokens(output);
1600
- tokensPromptTotal += promptTokens;
1601
- tokensCompletionTotal += completionTokens;
1602
- promptEstimateTotal += promptTokens;
1603
- await this.recordTokenUsage({
1604
- agentId: agent.id,
1605
- model: agent.defaultModel,
1606
- jobId: job.id,
1607
- commandRunId: commandRun.id,
1608
- taskRunId: taskRun.id,
1609
- taskId: task.task.id,
1610
- projectId: selection.project?.id,
1611
- tokensPrompt: promptTokens,
1612
- tokensCompletion: completionTokens,
1613
- phase,
1614
- durationSeconds,
1615
- });
1616
- };
1617
-
1618
- await recordUsage("agent", agentOutput, agentDuration, agentInput);
1619
-
1620
- let patches = extractPatches(agentOutput);
1621
- let fileBlocks = extractFileBlocks(agentOutput);
1622
- if (patches.length === 0 && fileBlocks.length === 0 && !triedRetry) {
1623
- triedRetry = true;
1624
- await this.logTask(taskRun.id, "Agent output did not include a patch or file blocks; retrying with explicit output instructions.", "agent");
1625
- try {
1626
- const retryInput = `${systemPrompt}\n\n${prompt}\n\nOutput only code changes. If editing existing files, output a unified diff inside \`\`\`patch\`\`\` fences. If creating new files, output FILE blocks in this format:\nFILE: path/to/file.ext\n\`\`\`\n<full file contents>\n\`\`\`\nDo not include analysis or narration.`;
1627
- const retry = await invokeAgentOnce(retryInput, "agent");
1628
- agentOutput = retry.output;
1629
- agentDuration += retry.durationSeconds;
1630
- await recordUsage("agent_retry", retry.output, retry.durationSeconds, retryInput);
1631
- patches = extractPatches(agentOutput);
1632
- fileBlocks = extractFileBlocks(agentOutput);
1633
- } catch (error) {
1634
- const message = error instanceof Error ? error.message : String(error);
1635
- await this.logTask(taskRun.id, `Agent retry failed: ${message}`, "agent");
1636
- }
1637
- }
1638
-
1639
- if (patches.length === 0 && fileBlocks.length === 0) {
1640
- const message = "Agent output did not include a patch or file blocks.";
1641
- await this.logTask(taskRun.id, message, "agent");
1642
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1643
- await this.stateService.markBlocked(task.task, "missing_patch");
1644
- results.push({ taskKey: task.task.key, status: "failed", notes: "missing_patch" });
1645
- taskStatus = "failed";
1646
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1647
- continue;
1648
- }
1649
-
1650
- if (patches.length || fileBlocks.length) {
1651
- if (!(await refreshLock("apply_start", true))) {
1652
- await this.logTask(taskRun.id, "Aborting task: lock lost before apply.", "vcs");
1653
- throw new Error("Task lock lost before apply.");
1654
- }
1655
- const applyDetails: Record<string, unknown> = {};
1656
- if (patches.length) applyDetails.patchCount = patches.length;
1657
- if (fileBlocks.length) applyDetails.fileCount = fileBlocks.length;
1658
- if (fileBlocks.length && !patches.length) applyDetails.mode = "direct";
1659
- await startPhase("apply", applyDetails);
1660
- let patchApplyError: string | null = null;
1661
- if (patches.length) {
1662
- const applied = await this.applyPatches(
1663
- patches,
1664
- this.workspace.workspaceRoot,
1665
- request.dryRun ?? false,
1666
- );
1667
- touched = applied.touched;
1668
- if (applied.warnings?.length) {
1669
- await this.logTask(taskRun.id, applied.warnings.join("; "), "patch");
1670
- }
1671
- if (applied.error) {
1672
- patchApplyError = applied.error;
1673
- await this.logTask(taskRun.id, `Patch apply failed: ${applied.error}`, "patch");
1674
- if (!fileBlocks.length) {
1675
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, "apply", "error", { error: applied.error });
1676
- await this.stateService.markBlocked(task.task, "patch_failed");
1677
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1678
- results.push({ taskKey: task.task.key, status: "failed", notes: "patch_failed" });
1679
- taskStatus = "failed";
1680
- if (!request.dryRun && request.noCommit !== true) {
1681
- await this.commitPendingChanges(
1682
- branchInfo,
1683
- task.task.key,
1684
- task.task.title,
1685
- "auto-save (patch_failed)",
1686
- task.task.id,
1687
- taskRun.id,
1688
- );
1689
- }
1690
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1691
- continue;
1692
- }
1693
- }
1694
- }
1695
- if (fileBlocks.length) {
1696
- const allowNoop = patchApplyError === null && touched.length > 0;
1697
- const applied = await this.applyFileBlocks(fileBlocks, this.workspace.workspaceRoot, request.dryRun ?? false, allowNoop);
1698
- if (applied.touched.length) {
1699
- const merged = new Set([...touched, ...applied.touched]);
1700
- touched = Array.from(merged);
1701
- }
1702
- if (applied.warnings?.length) {
1703
- await this.logTask(taskRun.id, applied.warnings.join("; "), "patch");
1704
- }
1705
- if (applied.error) {
1706
- await this.logTask(taskRun.id, `Direct file apply failed: ${applied.error}`, "patch");
1707
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, "apply", "error", { error: applied.error });
1708
- await this.stateService.markBlocked(task.task, "patch_failed");
1709
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1710
- results.push({ taskKey: task.task.key, status: "failed", notes: "patch_failed" });
1711
- taskStatus = "failed";
1712
- if (!request.dryRun && request.noCommit !== true) {
1713
- await this.commitPendingChanges(
1714
- branchInfo,
1715
- task.task.key,
1716
- task.task.title,
1717
- "auto-save (patch_failed)",
1718
- task.task.id,
1719
- taskRun.id,
1720
- );
1721
- }
1722
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1723
- continue;
1724
- }
1725
- if (patchApplyError && applied.appliedCount > 0) {
1726
- await this.logTask(
1727
- taskRun.id,
1728
- `Patch apply skipped; continued with file blocks. Reason: ${patchApplyError}`,
1729
- "patch",
1730
- );
1731
- patchApplyError = null;
1732
- }
1733
- }
1734
- if (patchApplyError) {
1735
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, "apply", "error", { error: patchApplyError });
1736
- await this.stateService.markBlocked(task.task, "patch_failed");
1737
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1738
- results.push({ taskKey: task.task.key, status: "failed", notes: "patch_failed" });
1739
- taskStatus = "failed";
1740
- if (!request.dryRun && request.noCommit !== true) {
1741
- await this.commitPendingChanges(
1742
- branchInfo,
1743
- task.task.key,
1744
- task.task.title,
1745
- "auto-save (patch_failed)",
1746
- task.task.id,
1747
- taskRun.id,
1748
- );
1749
- }
1750
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1751
- continue;
1752
- }
1753
- patchApplied = touched.length > 0;
1754
- await endPhase("apply", { touched });
1755
- if (!(await refreshLock("apply"))) {
1756
- await this.logTask(taskRun.id, "Aborting task: lock lost after apply.", "vcs");
1757
- throw new Error("Task lock lost after apply.");
1758
- }
1759
- }
1760
-
1761
- if (patchApplied && allowedFiles.length) {
1762
- const dirtyAfterApply = (await this.vcs.dirtyPaths(this.workspace.workspaceRoot)).filter((p) => !p.startsWith(".mcoda"));
1763
- const scopeCheck = this.validateScope(allowedFiles, normalizePaths(this.workspace.workspaceRoot, dirtyAfterApply));
1764
- if (!scopeCheck.ok) {
1765
- await this.logTask(taskRun.id, scopeCheck.message ?? "Scope violation", "scope");
1766
- await this.stateService.markBlocked(task.task, "scope_violation");
1767
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1768
- results.push({ taskKey: task.task.key, status: "failed", notes: "scope_violation" });
1769
- taskStatus = "failed";
1770
- if (!request.dryRun && request.noCommit !== true && patchApplied) {
1771
- await this.commitPendingChanges(branchInfo, task.task.key, task.task.title, "auto-save (scope_violation)", task.task.id, taskRun.id);
1772
- }
1773
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1774
- continue;
1775
- }
1776
- }
1777
-
1778
- if (!request.dryRun && testCommands.length && patchApplied) {
1779
- await startPhase("tests", { commands: testCommands });
1780
- const testResult = await this.runTests(testCommands, this.workspace.workspaceRoot);
1781
- await this.logTask(taskRun.id, "Test results", "tests", { results: testResult.results });
1782
- if (!testResult.ok) {
1783
- await this.logTask(taskRun.id, "Tests failed; continuing task run with warnings.", "tests");
1784
- softFailures.push("tests_failed");
1785
- await endPhase("tests", { results: testResult.results, warning: "tests_failed" });
1786
- } else {
1787
- await endPhase("tests", { results: testResult.results });
1788
- }
1789
- if (!(await refreshLock("tests"))) {
1790
- await this.logTask(taskRun.id, "Aborting task: lock lost after tests.", "vcs");
1791
- throw new Error("Task lock lost after tests.");
1792
- }
1793
- }
1794
-
1795
- if (!request.dryRun && request.noCommit !== true) {
1796
- if (!(await refreshLock("vcs_start", true))) {
1797
- await this.logTask(taskRun.id, "Aborting task: lock lost before VCS phase.", "vcs");
1798
- throw new Error("Task lock lost before VCS phase.");
1799
- }
1800
- await startPhase("vcs", { branch: branchInfo.branch, base: branchInfo.base });
1801
- try {
1802
- const dirty = (await this.vcs.dirtyPaths(this.workspace.workspaceRoot)).filter((p) => !p.startsWith(".mcoda"));
1803
- const toStage = dirty.length ? dirty : touched.length ? touched : ["."];
1804
- await this.vcs.stage(this.workspace.workspaceRoot, toStage);
1805
- const status = await this.vcs.status(this.workspace.workspaceRoot);
1806
- const hasChanges = status.trim().length > 0;
1807
- if (hasChanges) {
1808
- const commitMessage = `[${task.task.key}] ${task.task.title}`;
1809
- let committed = false;
1810
- try {
1811
- await this.vcs.commit(this.workspace.workspaceRoot, commitMessage);
1812
- committed = true;
1813
- } catch (error) {
1814
- const errorText = this.formatGitError(error);
1815
- const hookFailure = this.isCommitHookFailure(errorText);
1816
- const gpgFailure = this.isGpgSignFailure(errorText);
1817
- if (hookFailure || gpgFailure) {
1818
- const guidance = [
1819
- hookFailure
1820
- ? "Commit hook failed; run hooks manually or configure bypass (e.g., HUSKY=0) if policy allows."
1821
- : "",
1822
- gpgFailure
1823
- ? "GPG signing failed; configure signing key or disable commit.gpgsign for this repo."
1824
- : "",
1825
- ]
1826
- .filter(Boolean)
1827
- .join(" ");
1828
- await this.logTask(taskRun.id, `Commit failed; retrying with bypass flags. ${guidance}`, "vcs", {
1829
- error: errorText,
1830
- });
1831
- try {
1832
- await this.vcs.commit(this.workspace.workspaceRoot, commitMessage, {
1833
- noVerify: hookFailure,
1834
- noGpgSign: gpgFailure,
1835
- });
1836
- committed = true;
1837
- await this.logTask(taskRun.id, "Commit succeeded after bypassing hook/signing checks.", "vcs");
1838
- } catch (retryError) {
1839
- const retryText = this.formatGitError(retryError);
1840
- throw new Error(`Commit failed after retry: ${retryText}`);
1841
- }
1842
- } else {
1843
- throw error;
1844
- }
1845
- }
1846
- if (committed) {
1847
- const head = await this.vcs.lastCommitSha(this.workspace.workspaceRoot);
1848
- await this.deps.workspaceRepo.updateTask(task.task.id, { vcsLastCommitSha: head });
1849
- await this.logTask(taskRun.id, `Committed changes (${head})`, "vcs");
1850
- headSha = head;
1851
- }
1852
- } else {
1853
- await this.logTask(taskRun.id, "No changes to commit.", "vcs");
1854
- }
1855
-
1856
- // Always merge back into base and end on base branch.
1857
- try {
1858
- await this.vcs.merge(this.workspace.workspaceRoot, branchInfo.branch, branchInfo.base);
1859
- mergeStatus = "merged";
1860
- try {
1861
- headSha = await this.vcs.lastCommitSha(this.workspace.workspaceRoot);
1862
- } catch {
1863
- // Best-effort head capture.
1864
- }
1865
- await this.logTask(taskRun.id, `Merged ${branchInfo.branch} into ${branchInfo.base}`, "vcs");
1866
- if (!(await refreshLock("vcs_merge"))) {
1867
- await this.logTask(taskRun.id, "Aborting task: lock lost after merge.", "vcs");
1868
- throw new Error("Task lock lost after merge.");
1869
- }
1870
- } catch (error) {
1871
- mergeStatus = "failed";
1872
- const conflicts = await this.vcs.conflictPaths(this.workspace.workspaceRoot);
1873
- if (conflicts.length) {
1874
- await this.logTask(taskRun.id, `Merge conflicts while merging ${branchInfo.branch} into ${branchInfo.base}.`, "vcs", {
1875
- conflicts,
1876
- });
1877
- throw new Error(
1878
- `Merge conflict(s) while merging ${branchInfo.branch} into ${branchInfo.base}: ${conflicts.join(", ")}`,
1879
- );
1880
- }
1881
- throw error;
1882
- }
1883
-
1884
- if (await this.vcs.hasRemote(this.workspace.workspaceRoot)) {
1885
- const branchPush = await this.pushWithRecovery(taskRun.id, branchInfo.branch);
1886
- if (branchPush.pushed) {
1887
- await this.logTask(taskRun.id, "Pushed branch to remote origin", "vcs");
1888
- } else if (branchPush.skipped) {
1889
- await this.logTask(taskRun.id, "Skipped pushing branch to remote origin due to permissions/protection.", "vcs");
1890
- }
1891
- if (!(await refreshLock("vcs_push_branch"))) {
1892
- await this.logTask(taskRun.id, "Aborting task: lock lost after pushing branch.", "vcs");
1893
- throw new Error("Task lock lost after pushing branch.");
1894
- }
1895
- const basePush = await this.pushWithRecovery(taskRun.id, branchInfo.base);
1896
- if (basePush.pushed) {
1897
- await this.logTask(taskRun.id, `Pushed base branch ${branchInfo.base} to remote origin`, "vcs");
1898
- } else if (basePush.skipped) {
1899
- await this.logTask(taskRun.id, `Skipped pushing base branch ${branchInfo.base} due to permissions/protection.`, "vcs");
1900
- }
1901
- if (!(await refreshLock("vcs_push_base"))) {
1902
- await this.logTask(taskRun.id, "Aborting task: lock lost after pushing base branch.", "vcs");
1903
- throw new Error("Task lock lost after pushing base branch.");
1904
- }
1905
- } else {
1906
- await this.logTask(taskRun.id, "No remote configured; merge completed locally.", "vcs");
1907
- }
1908
- } catch (error) {
1909
- const message = error instanceof Error ? error.message : String(error);
1910
- if (/task lock lost/i.test(message)) {
1911
- throw error;
1912
- }
1913
- await this.logTask(taskRun.id, `VCS commit/push failed: ${message}`, "vcs");
1914
- await this.updateTaskPhase(job.id, taskRun.id, task.task.key, "vcs", "error", { error: message });
1915
- await this.stateService.markBlocked(task.task, "vcs_failed");
1916
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1917
- results.push({ taskKey: task.task.key, status: "failed", notes: "vcs_failed" });
1918
- taskStatus = "failed";
1919
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1920
- continue;
1921
- }
1922
- await endPhase("vcs", { branch: branchInfo.branch, base: branchInfo.base });
1923
- } else if (request.dryRun) {
1924
- await this.logTask(taskRun.id, "Dry-run: skipped commit/push.", "vcs");
1925
- } else if (request.noCommit) {
1926
- await this.logTask(taskRun.id, "no-commit set: skipped commit/push.", "vcs");
1927
- }
1928
-
1929
- await startPhase("finalize");
1930
- const finishedAt = new Date().toISOString();
1931
- const elapsedSeconds = Math.max(1, (Date.parse(finishedAt) - Date.parse(startedAt)) / 1000);
1932
- const spPerHour =
1933
- task.task.storyPoints && task.task.storyPoints > 0 ? (task.task.storyPoints / elapsedSeconds) * 3600 : null;
1934
-
1935
- const reviewMetadata: Record<string, unknown> = { last_run: finishedAt };
1936
- if (softFailures.length) {
1937
- reviewMetadata.soft_failures = softFailures;
1938
- }
1939
- await this.stateService.markReadyToReview(task.task, reviewMetadata);
1940
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
1941
- status: "succeeded",
1942
- finishedAt,
1943
- spPerHourEffective: spPerHour,
1944
- gitBranch: branchInfo.branch,
1945
- gitBaseBranch: branchInfo.base,
1946
- });
1947
-
1948
- storyPointsProcessed += task.task.storyPoints ?? 0;
1949
- await endPhase("finalize", { spPerHour: spPerHour ?? undefined });
1950
-
1951
- const resultNotes = softFailures.length ? `ready_to_review_with_warnings:${softFailures.join(",")}` : "ready_to_review";
1952
- taskStatus = "succeeded";
1953
- results.push({
1954
- taskKey: task.task.key,
1955
- status: "succeeded",
1956
- notes: resultNotes,
1957
- branch: branchInfo.branch,
1958
- });
1959
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1960
- await this.checkpoint(job.id, "task_ready_for_review", { taskKey: task.task.key });
1961
- } catch (error) {
1962
- const message = error instanceof Error ? error.message : String(error);
1963
- if (/task lock lost/i.test(message)) {
1964
- await this.logTask(taskRun.id, `Task aborted: ${message}`, "vcs");
1965
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, { status: "failed", finishedAt: new Date().toISOString() });
1966
- await this.stateService.markBlocked(task.task, "task_lock_lost");
1967
- if (!request.dryRun && request.noCommit !== true) {
1968
- await this.commitPendingChanges(branchInfo, task.task.key, task.task.title, "auto-save (lock_lost)", task.task.id, taskRun.id);
1969
- }
1970
- results.push({ taskKey: task.task.key, status: "failed", notes: "task_lock_lost" });
1971
- taskStatus = "failed";
1972
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
1973
- continue;
1974
- }
1975
- throw error;
1976
- } finally {
1977
- await emitTaskEndOnce();
1978
- if (lockAcquired) {
1979
- await this.deps.workspaceRepo.releaseTaskLock(task.task.id, taskRun.id);
1980
- }
1981
- }
1982
- }
1983
-
1984
- const failureCount = results.filter((r) => r.status === "failed" || r.status === "blocked").length;
1985
- const state: JobState =
1986
- failureCount === 0 ? "completed" : failureCount === results.length ? "failed" : ("partial" as JobState);
1987
- const errorSummary = failureCount ? `${failureCount} task(s) failed or blocked` : undefined;
1988
- await this.deps.jobService.updateJobStatus(job.id, state, {
1989
- processedItems: results.length,
1990
- errorSummary,
1991
- });
1992
- await this.deps.jobService.finishCommandRun(
1993
- commandRun.id,
1994
- state === "completed" ? "succeeded" : "failed",
1995
- errorSummary,
1996
- storyPointsProcessed || undefined,
1997
- );
1998
-
1999
- return {
2000
- jobId: job.id,
2001
- commandRunId: commandRun.id,
2002
- selection,
2003
- results,
2004
- warnings,
2005
- };
2006
- } catch (error) {
2007
- const message = error instanceof Error ? error.message : String(error);
2008
- await this.deps.jobService.updateJobStatus(job.id, "failed", {
2009
- processedItems: undefined,
2010
- errorSummary: message,
2011
- });
2012
- await this.deps.jobService.finishCommandRun(commandRun.id, "failed", message, storyPointsProcessed || undefined);
2013
- throw error;
2014
- } finally {
2015
- // Best-effort return to base branch after processing.
2016
- try {
2017
- await this.vcs.checkoutBranch(this.workspace.workspaceRoot, baseBranch);
2018
- } catch {
2019
- // ignore if checkout fails (e.g., dirty tree); user can resolve manually.
2020
- }
2021
- }
2022
- }
2023
- }