vibeman 0.0.5 → 0.0.6

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 (241) hide show
  1. package/dist/api.js +49 -0
  2. package/dist/cli.js +135 -0
  3. package/dist/ui/index-gnk6rhxs.js +9 -0
  4. package/dist/ui/index.html +10 -0
  5. package/dist/ui/index.js +2 -0
  6. package/package.json +10 -46
  7. package/README.md +0 -12
  8. package/dist/index.js +0 -114
  9. package/dist/runtime/api/.tsbuildinfo +0 -1
  10. package/dist/runtime/api/agent/agent-service.d.ts +0 -229
  11. package/dist/runtime/api/agent/agent-service.js +0 -963
  12. package/dist/runtime/api/agent/ai-providers/amp-cli-provider.d.ts +0 -38
  13. package/dist/runtime/api/agent/ai-providers/amp-cli-provider.js +0 -268
  14. package/dist/runtime/api/agent/ai-providers/claude-code-adapter.d.ts +0 -61
  15. package/dist/runtime/api/agent/ai-providers/claude-code-adapter.js +0 -362
  16. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.d.ts +0 -36
  17. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +0 -375
  18. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.d.ts +0 -24
  19. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.js +0 -291
  20. package/dist/runtime/api/agent/ai-providers/index.d.ts +0 -9
  21. package/dist/runtime/api/agent/ai-providers/index.js +0 -9
  22. package/dist/runtime/api/agent/ai-providers/types.d.ts +0 -185
  23. package/dist/runtime/api/agent/ai-providers/types.js +0 -5
  24. package/dist/runtime/api/agent/amp-cli-provider.test.d.ts +0 -1
  25. package/dist/runtime/api/agent/amp-cli-provider.test.js +0 -99
  26. package/dist/runtime/api/agent/codex-cli-provider.test.d.ts +0 -1
  27. package/dist/runtime/api/agent/codex-cli-provider.test.js +0 -172
  28. package/dist/runtime/api/agent/core-agent-service.d.ts +0 -119
  29. package/dist/runtime/api/agent/core-agent-service.js +0 -267
  30. package/dist/runtime/api/agent/parsers.d.ts +0 -16
  31. package/dist/runtime/api/agent/parsers.js +0 -308
  32. package/dist/runtime/api/agent/prompt-service.d.ts +0 -30
  33. package/dist/runtime/api/agent/prompt-service.js +0 -452
  34. package/dist/runtime/api/agent/prompt-service.test.d.ts +0 -1
  35. package/dist/runtime/api/agent/prompt-service.test.js +0 -265
  36. package/dist/runtime/api/agent/routing-policy.d.ts +0 -171
  37. package/dist/runtime/api/agent/routing-policy.js +0 -196
  38. package/dist/runtime/api/agent/routing-policy.test.d.ts +0 -1
  39. package/dist/runtime/api/agent/routing-policy.test.js +0 -63
  40. package/dist/runtime/api/api/router-helpers.d.ts +0 -32
  41. package/dist/runtime/api/api/router-helpers.js +0 -31
  42. package/dist/runtime/api/api/routers/ai.d.ts +0 -200
  43. package/dist/runtime/api/api/routers/ai.js +0 -396
  44. package/dist/runtime/api/api/routers/executions.d.ts +0 -93
  45. package/dist/runtime/api/api/routers/executions.js +0 -94
  46. package/dist/runtime/api/api/routers/git.d.ts +0 -45
  47. package/dist/runtime/api/api/routers/git.js +0 -35
  48. package/dist/runtime/api/api/routers/provider-config.d.ts +0 -199
  49. package/dist/runtime/api/api/routers/provider-config.js +0 -252
  50. package/dist/runtime/api/api/routers/settings.d.ts +0 -158
  51. package/dist/runtime/api/api/routers/settings.js +0 -129
  52. package/dist/runtime/api/api/routers/tasks.d.ts +0 -141
  53. package/dist/runtime/api/api/routers/tasks.js +0 -238
  54. package/dist/runtime/api/api/routers/workflows.d.ts +0 -275
  55. package/dist/runtime/api/api/routers/workflows.js +0 -311
  56. package/dist/runtime/api/api/routers/worktrees.d.ts +0 -101
  57. package/dist/runtime/api/api/routers/worktrees.js +0 -80
  58. package/dist/runtime/api/api/trpc.d.ts +0 -118
  59. package/dist/runtime/api/api/trpc.js +0 -34
  60. package/dist/runtime/api/index.d.ts +0 -9
  61. package/dist/runtime/api/index.js +0 -117
  62. package/dist/runtime/api/lib/id-generator.d.ts +0 -70
  63. package/dist/runtime/api/lib/id-generator.js +0 -123
  64. package/dist/runtime/api/lib/local-config.d.ts +0 -335
  65. package/dist/runtime/api/lib/local-config.js +0 -304
  66. package/dist/runtime/api/lib/logger.d.ts +0 -11
  67. package/dist/runtime/api/lib/logger.js +0 -188
  68. package/dist/runtime/api/lib/provider-detection.d.ts +0 -61
  69. package/dist/runtime/api/lib/provider-detection.js +0 -326
  70. package/dist/runtime/api/lib/server/agent-service-singleton.d.ts +0 -6
  71. package/dist/runtime/api/lib/server/agent-service-singleton.js +0 -27
  72. package/dist/runtime/api/lib/server/bootstrap.d.ts +0 -38
  73. package/dist/runtime/api/lib/server/bootstrap.js +0 -197
  74. package/dist/runtime/api/lib/server/git-service-singleton.d.ts +0 -6
  75. package/dist/runtime/api/lib/server/git-service-singleton.js +0 -47
  76. package/dist/runtime/api/lib/server/project-root.d.ts +0 -2
  77. package/dist/runtime/api/lib/server/project-root.js +0 -61
  78. package/dist/runtime/api/lib/server/task-service-singleton.d.ts +0 -7
  79. package/dist/runtime/api/lib/server/task-service-singleton.js +0 -58
  80. package/dist/runtime/api/lib/server/vibeman-info.d.ts +0 -5
  81. package/dist/runtime/api/lib/server/vibeman-info.js +0 -85
  82. package/dist/runtime/api/lib/server/vibing-orchestrator-singleton.d.ts +0 -7
  83. package/dist/runtime/api/lib/server/vibing-orchestrator-singleton.js +0 -57
  84. package/dist/runtime/api/lib/trpc/server.d.ts +0 -965
  85. package/dist/runtime/api/lib/trpc/server.js +0 -11
  86. package/dist/runtime/api/lib/trpc/ws-server.d.ts +0 -8
  87. package/dist/runtime/api/lib/trpc/ws-server.js +0 -33
  88. package/dist/runtime/api/persistence/database-service.d.ts +0 -14
  89. package/dist/runtime/api/persistence/database-service.js +0 -74
  90. package/dist/runtime/api/persistence/execution-log-persistence.d.ts +0 -90
  91. package/dist/runtime/api/persistence/execution-log-persistence.js +0 -426
  92. package/dist/runtime/api/persistence/execution-log-persistence.test.d.ts +0 -1
  93. package/dist/runtime/api/persistence/execution-log-persistence.test.js +0 -170
  94. package/dist/runtime/api/router.d.ts +0 -968
  95. package/dist/runtime/api/router.js +0 -34
  96. package/dist/runtime/api/settings-service.d.ts +0 -110
  97. package/dist/runtime/api/settings-service.js +0 -678
  98. package/dist/runtime/api/tasks/file-watcher.d.ts +0 -23
  99. package/dist/runtime/api/tasks/file-watcher.js +0 -88
  100. package/dist/runtime/api/tasks/task-file-parser.d.ts +0 -14
  101. package/dist/runtime/api/tasks/task-file-parser.js +0 -180
  102. package/dist/runtime/api/tasks/task-service.d.ts +0 -36
  103. package/dist/runtime/api/tasks/task-service.js +0 -173
  104. package/dist/runtime/api/tasks/task-updater.d.ts +0 -62
  105. package/dist/runtime/api/tasks/task-updater.js +0 -260
  106. package/dist/runtime/api/tasks/task-updater.test.d.ts +0 -1
  107. package/dist/runtime/api/tasks/task-updater.test.js +0 -303
  108. package/dist/runtime/api/types/index.d.ts +0 -186
  109. package/dist/runtime/api/types/index.js +0 -1
  110. package/dist/runtime/api/types/settings.d.ts +0 -105
  111. package/dist/runtime/api/types/settings.js +0 -2
  112. package/dist/runtime/api/types.d.ts +0 -2
  113. package/dist/runtime/api/types.js +0 -1
  114. package/dist/runtime/api/utils/env.d.ts +0 -6
  115. package/dist/runtime/api/utils/env.js +0 -12
  116. package/dist/runtime/api/utils/stripNextEnv.d.ts +0 -7
  117. package/dist/runtime/api/utils/stripNextEnv.js +0 -22
  118. package/dist/runtime/api/utils/title-slug.d.ts +0 -6
  119. package/dist/runtime/api/utils/title-slug.js +0 -77
  120. package/dist/runtime/api/utils/url.d.ts +0 -2
  121. package/dist/runtime/api/utils/url.js +0 -19
  122. package/dist/runtime/api/vcs/git-history-service.d.ts +0 -57
  123. package/dist/runtime/api/vcs/git-history-service.js +0 -228
  124. package/dist/runtime/api/vcs/git-service.d.ts +0 -136
  125. package/dist/runtime/api/vcs/git-service.js +0 -307
  126. package/dist/runtime/api/vcs/worktree-service.d.ts +0 -93
  127. package/dist/runtime/api/vcs/worktree-service.js +0 -518
  128. package/dist/runtime/api/vcs/worktree-service.test.d.ts +0 -1
  129. package/dist/runtime/api/vcs/worktree-service.test.js +0 -20
  130. package/dist/runtime/api/workflows/quality-pipeline.d.ts +0 -58
  131. package/dist/runtime/api/workflows/quality-pipeline.js +0 -401
  132. package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +0 -406
  133. package/dist/runtime/api/workflows/vibing-orchestrator.js +0 -2462
  134. package/dist/runtime/api/workflows/workflow-effects.d.ts +0 -45
  135. package/dist/runtime/api/workflows/workflow-effects.js +0 -49
  136. package/dist/runtime/api/workflows/workflow-reconciler.d.ts +0 -65
  137. package/dist/runtime/api/workflows/workflow-reconciler.js +0 -226
  138. package/dist/runtime/api/workflows/workflow-reducer.d.ts +0 -26
  139. package/dist/runtime/api/workflows/workflow-reducer.js +0 -288
  140. package/dist/runtime/api/workflows/workflow-reducer.test.d.ts +0 -1
  141. package/dist/runtime/api/workflows/workflow-reducer.test.js +0 -247
  142. package/dist/runtime/api/workflows/workflow-schema.d.ts +0 -546
  143. package/dist/runtime/api/workflows/workflow-schema.js +0 -256
  144. package/dist/runtime/web/.next/BUILD_ID +0 -1
  145. package/dist/runtime/web/.next/app-build-manifest.json +0 -66
  146. package/dist/runtime/web/.next/app-path-routes-manifest.json +0 -8
  147. package/dist/runtime/web/.next/build-manifest.json +0 -33
  148. package/dist/runtime/web/.next/package.json +0 -1
  149. package/dist/runtime/web/.next/prerender-manifest.json +0 -61
  150. package/dist/runtime/web/.next/react-loadable-manifest.json +0 -8
  151. package/dist/runtime/web/.next/required-server-files.json +0 -334
  152. package/dist/runtime/web/.next/routes-manifest.json +0 -70
  153. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +0 -1
  154. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +0 -1
  155. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +0 -1
  156. package/dist/runtime/web/.next/server/app/_not-found/page.js +0 -2
  157. package/dist/runtime/web/.next/server/app/_not-found/page.js.nft.json +0 -1
  158. package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  159. package/dist/runtime/web/.next/server/app/_not-found.html +0 -7
  160. package/dist/runtime/web/.next/server/app/_not-found.meta +0 -8
  161. package/dist/runtime/web/.next/server/app/_not-found.rsc +0 -22
  162. package/dist/runtime/web/.next/server/app/api/health/route.js +0 -1
  163. package/dist/runtime/web/.next/server/app/api/health/route.js.nft.json +0 -1
  164. package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +0 -1
  165. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +0 -1
  166. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js.nft.json +0 -1
  167. package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +0 -1
  168. package/dist/runtime/web/.next/server/app/api/upload/route.js +0 -1
  169. package/dist/runtime/web/.next/server/app/api/upload/route.js.nft.json +0 -1
  170. package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +0 -1
  171. package/dist/runtime/web/.next/server/app/index.html +0 -7
  172. package/dist/runtime/web/.next/server/app/index.meta +0 -7
  173. package/dist/runtime/web/.next/server/app/index.rsc +0 -27
  174. package/dist/runtime/web/.next/server/app/page.js +0 -112
  175. package/dist/runtime/web/.next/server/app/page.js.nft.json +0 -1
  176. package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +0 -1
  177. package/dist/runtime/web/.next/server/app-paths-manifest.json +0 -8
  178. package/dist/runtime/web/.next/server/chunks/210.js +0 -1
  179. package/dist/runtime/web/.next/server/chunks/291.js +0 -18
  180. package/dist/runtime/web/.next/server/chunks/552.js +0 -22
  181. package/dist/runtime/web/.next/server/chunks/780.js +0 -1
  182. package/dist/runtime/web/.next/server/chunks/905.js +0 -6
  183. package/dist/runtime/web/.next/server/chunks/98.js +0 -1
  184. package/dist/runtime/web/.next/server/functions-config-manifest.json +0 -4
  185. package/dist/runtime/web/.next/server/middleware-build-manifest.js +0 -1
  186. package/dist/runtime/web/.next/server/middleware-manifest.json +0 -6
  187. package/dist/runtime/web/.next/server/middleware-react-loadable-manifest.js +0 -1
  188. package/dist/runtime/web/.next/server/next-font-manifest.js +0 -1
  189. package/dist/runtime/web/.next/server/next-font-manifest.json +0 -1
  190. package/dist/runtime/web/.next/server/pages/404.html +0 -7
  191. package/dist/runtime/web/.next/server/pages/500.html +0 -1
  192. package/dist/runtime/web/.next/server/pages/_app.js +0 -1
  193. package/dist/runtime/web/.next/server/pages/_app.js.nft.json +0 -1
  194. package/dist/runtime/web/.next/server/pages/_document.js +0 -1
  195. package/dist/runtime/web/.next/server/pages/_document.js.nft.json +0 -1
  196. package/dist/runtime/web/.next/server/pages/_error.js +0 -19
  197. package/dist/runtime/web/.next/server/pages/_error.js.nft.json +0 -1
  198. package/dist/runtime/web/.next/server/pages-manifest.json +0 -6
  199. package/dist/runtime/web/.next/server/server-reference-manifest.js +0 -1
  200. package/dist/runtime/web/.next/server/server-reference-manifest.json +0 -1
  201. package/dist/runtime/web/.next/server/webpack-runtime.js +0 -1
  202. package/dist/runtime/web/.next/static/LJFZk_8tvKFN_Ee4HqUuM/_buildManifest.js +0 -1
  203. package/dist/runtime/web/.next/static/LJFZk_8tvKFN_Ee4HqUuM/_ssgManifest.js +0 -1
  204. package/dist/runtime/web/.next/static/chunks/05c91ade-7d09b2b280adffd1.js +0 -1
  205. package/dist/runtime/web/.next/static/chunks/201-51bef3fa8c832e2e.js +0 -1
  206. package/dist/runtime/web/.next/static/chunks/524-89747ed9b0294f8a.js +0 -1
  207. package/dist/runtime/web/.next/static/chunks/554-8bec6e9cca6acc67.js +0 -1
  208. package/dist/runtime/web/.next/static/chunks/764.86e9503a69d45a85.js +0 -1
  209. package/dist/runtime/web/.next/static/chunks/7ab4dc20-239138e0ae7af24a.js +0 -1
  210. package/dist/runtime/web/.next/static/chunks/905-342391e3d3a3678f.js +0 -20
  211. package/dist/runtime/web/.next/static/chunks/a8a5ce16-4edea7df2d9b544a.js +0 -79
  212. package/dist/runtime/web/.next/static/chunks/ad74d572-4c1b162e2c15acaa.js +0 -1
  213. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-7b752a8641f96c1f.js +0 -1
  214. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-34e66b251c2b5044.js +0 -1
  215. package/dist/runtime/web/.next/static/chunks/app/api/health/route-7b752a8641f96c1f.js +0 -1
  216. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-7b752a8641f96c1f.js +0 -1
  217. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-7b752a8641f96c1f.js +0 -1
  218. package/dist/runtime/web/.next/static/chunks/app/layout-df9ac93cb02b2385.js +0 -1
  219. package/dist/runtime/web/.next/static/chunks/app/page-6610743f7de5f92a.js +0 -1
  220. package/dist/runtime/web/.next/static/chunks/c25e0690-e9b798b8de667da1.js +0 -1
  221. package/dist/runtime/web/.next/static/chunks/framework-57157ec4d37f64aa.js +0 -1
  222. package/dist/runtime/web/.next/static/chunks/main-app-156cc0c60371bd78.js +0 -1
  223. package/dist/runtime/web/.next/static/chunks/main-df25d367c47b1fec.js +0 -1
  224. package/dist/runtime/web/.next/static/chunks/pages/_app-9f629a5e1131d19f.js +0 -1
  225. package/dist/runtime/web/.next/static/chunks/pages/_error-9238238274c7efcd.js +0 -1
  226. package/dist/runtime/web/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  227. package/dist/runtime/web/.next/static/chunks/webpack-cd50e39b423d1808.js +0 -1
  228. package/dist/runtime/web/.next/static/css/2728291c68f99cb1.css +0 -3
  229. package/dist/runtime/web/.next/static/css/4fbf378a264bd4ea.css +0 -1
  230. package/dist/runtime/web/.next/static/css/521bd69cc298cd1a.css +0 -1
  231. package/dist/runtime/web/.next/static/css/537e22821e101b87.css +0 -1
  232. package/dist/runtime/web/.next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  233. package/dist/runtime/web/.next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  234. package/dist/runtime/web/.next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  235. package/dist/runtime/web/.next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  236. package/dist/runtime/web/.next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  237. package/dist/runtime/web/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  238. package/dist/runtime/web/.next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  239. package/dist/runtime/web/package.json +0 -65
  240. package/dist/runtime/web/server.js +0 -44
  241. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,2462 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { TaskUpdater } from '../tasks/task-updater.js';
3
- import { QualityPipeline } from './quality-pipeline.js';
4
- import { DatabaseService } from '../persistence/database-service.js';
5
- import { log } from '../lib/logger.js';
6
- import { generateId } from '../lib/id-generator.js';
7
- import path from 'path';
8
- import { stripNextInjectedEnv } from '../utils/stripNextEnv.js';
9
- import { spawn } from 'child_process';
10
- import { getSettingsService } from '../settings-service.js';
11
- import { getVibeDir } from '../lib/server/project-root.js';
12
- import { EXTENDED_PHASE_FLOW, EXTENDED_PHASE_LABELS } from './workflow-schema.js';
13
- import { reduce, computeNextAction } from './workflow-reducer.js';
14
- import { executeEffects } from './workflow-effects.js';
15
- import { WorkflowReconciler, HeartbeatTracker, IdempotencyGuard, } from './workflow-reconciler.js';
16
- // Use unified phase definitions from workflow-schema
17
- const PHASE_FLOW = EXTENDED_PHASE_FLOW;
18
- // Extended labels with legacy mappings for timeline display
19
- const PHASE_LABELS = {
20
- ...EXTENDED_PHASE_LABELS,
21
- draft: 'Workflow started',
22
- implementing: 'Implementation',
23
- validating: 'Validation',
24
- merging: 'Merge',
25
- approved: 'Approved',
26
- };
27
- // Default config
28
- const getDefaultConfig = async () => {
29
- try {
30
- const settingsService = getSettingsService();
31
- await settingsService.initialize();
32
- const settings = settingsService.getSettings();
33
- return {
34
- autoQualityChecks: settings.defaultWorkflow.autoQualityChecks,
35
- requireHumanApproval: settings.defaultWorkflow.requireHumanApproval,
36
- aiCodeReview: settings.defaultWorkflow.aiCodeReview,
37
- stepByStepMode: false,
38
- retryPolicy: {
39
- maxImplementationAttempts: settings.defaultWorkflow.maxImplementationAttempts,
40
- },
41
- autoCommit: settings.defaultWorkflow.autoCommit,
42
- };
43
- }
44
- catch (error) {
45
- log.warn('Failed to load settings, using fallback defaults', error, 'vibing-orchestrator');
46
- return {
47
- autoQualityChecks: true,
48
- requireHumanApproval: true,
49
- aiCodeReview: true,
50
- stepByStepMode: false,
51
- retryPolicy: { maxImplementationAttempts: 3 },
52
- autoCommit: false,
53
- };
54
- }
55
- };
56
- export class VibingOrchestrator extends EventEmitter {
57
- // Build a new, visible timeline array without mutating the input
58
- // Correct order: remove placeholders -> add real phases -> fill missing placeholders
59
- computeVisibleTimeline(wf) {
60
- const base = Array.isArray(wf.timeline) ? [...wf.timeline] : [];
61
- // Visible phases in timeline
62
- const visible = new Set([
63
- 'draft',
64
- 'implementing',
65
- 'validating',
66
- 'ai-reviewing',
67
- 'awaiting-review',
68
- 'merging',
69
- 'cleaning',
70
- 'failed',
71
- 'paused',
72
- ]);
73
- // Ensure a start marker exists (non-placeholder)
74
- let startItem = base.find((t) => t.phase === 'draft' && !t.placeholder);
75
- if (!startItem && wf.startTime) {
76
- startItem = {
77
- id: `start:${wf.startTime}`,
78
- label: 'Workflow started',
79
- phase: 'draft',
80
- startTime: wf.startTime,
81
- };
82
- }
83
- // Remove all placeholders and keep only visible phases
84
- const realItems = base.filter((t) => !t.placeholder && visible.has(t.phase));
85
- // Group real items by phase (preserve per-phase chronological order)
86
- // TODO: retry logic is different from grouping by phase
87
- const realByPhase = new Map();
88
- for (const item of realItems) {
89
- const ph = item.phase;
90
- if (!realByPhase.has(ph))
91
- realByPhase.set(ph, []);
92
- realByPhase.get(ph).push(item);
93
- }
94
- for (const [, items] of realByPhase) {
95
- items.sort((a, b) => {
96
- const ta = new Date(a.startTime || wf.startTime || 0).getTime();
97
- const tb = new Date(b.startTime || wf.startTime || 0).getTime();
98
- return ta - tb;
99
- });
100
- }
101
- // Compose final ordered list: start -> each phase in PHASE_FLOW (real items if any, else placeholder)
102
- const out = [];
103
- if (startItem)
104
- out.push(startItem);
105
- const ts = wf.metrics?.lastPhaseStartedAt ||
106
- wf.lastUpdatedAt ||
107
- wf.startTime ||
108
- new Date().toISOString();
109
- for (const ph of PHASE_FLOW) {
110
- const items = realByPhase.get(ph) || [];
111
- if (items.length > 0) {
112
- out.push(...items);
113
- }
114
- else {
115
- out.push({
116
- id: `placeholder:${ph}`,
117
- label: PHASE_LABELS[ph],
118
- phase: ph,
119
- startTime: ts,
120
- placeholder: true,
121
- });
122
- }
123
- }
124
- // Append any other visible real items not in PHASE_FLOW (e.g., 'awaiting-review', 'failed', 'paused')
125
- for (const [ph, items] of realByPhase) {
126
- if (!PHASE_FLOW.includes(ph) && ph !== 'draft') {
127
- out.push(...items);
128
- }
129
- }
130
- return out;
131
- }
132
- // Build a new timeline by appending an optional item, then normalizing
133
- buildTimeline(wf, newItem) {
134
- const base = Array.isArray(wf.timeline) ? [...wf.timeline] : [];
135
- if (newItem)
136
- base.push(newItem);
137
- const tmp = { ...wf, timeline: base };
138
- return this.computeVisibleTimeline(tmp);
139
- }
140
- constructor(taskService, agentService, worktreeService, config, gitService, persistenceDataDir) {
141
- super();
142
- this.taskService = taskService;
143
- this.agentService = agentService;
144
- this.worktreeService = worktreeService;
145
- this.config = config;
146
- this.gitService = gitService;
147
- this.workflows = new Map();
148
- this.initialized = false;
149
- this.activeImprovements = new Map();
150
- this.taskImprovementExecutions = new Map();
151
- this.qualityPipeline = new QualityPipeline();
152
- this.taskUpdater = new TaskUpdater();
153
- // Persist workflows in a JSON file using the generic database service
154
- const filePath = persistenceDataDir
155
- ? path.join(persistenceDataDir, 'workflows.json')
156
- : path.join(getVibeDir(), '.local/workflows.json');
157
- this.workflowDB = new DatabaseService(filePath);
158
- // Initialize Phase 3: Reliability & Observability components
159
- this.heartbeatTracker = new HeartbeatTracker(5 * 60 * 1000); // 5 min stale threshold
160
- this.idempotencyGuard = new IdempotencyGuard(60 * 60 * 1000); // 1 hour TTL
161
- this.reconciler = new WorkflowReconciler({
162
- checkIntervalMs: 60 * 1000, // Check every minute
163
- stuckThresholdMs: 30 * 60 * 1000, // 30 min stuck threshold
164
- maxReconcileAttempts: 3,
165
- }, () => this.getWorkflowSnapshots(), (workflowId, action) => this.handleStuckWorkflow(workflowId, action));
166
- // Forward execution updates from AgentService so UIs can subscribe via orchestrator
167
- this.agentService.on('executionUpdated', (update) => {
168
- this.emit('executionUpdated', update);
169
- });
170
- // Listen for execution creation so we can associate executionId to workflows
171
- this.agentService.on('executionCreated', async (data) => {
172
- try {
173
- const wfId = data.workflowId;
174
- if (!wfId)
175
- return;
176
- const wf = this.workflows.get(wfId);
177
- if (!wf)
178
- return;
179
- // Track execution IDs and attempts for implementing phase
180
- const attempt = (wf.attempts?.implementing || 0) + 1;
181
- const executionIds = (wf.executionIds ?? (wf.executionIds = {}));
182
- const implList = (executionIds['implementing'] || []);
183
- executionIds['implementing'] = [...implList, data.executionId];
184
- wf.attempts = wf.attempts || {};
185
- wf.attempts['implementing'] = attempt;
186
- // Timeline: record implementation attempt start
187
- wf.timeline = this.buildTimeline(wf, {
188
- id: data.executionId,
189
- label: attempt > 1 ? `Implementation – Retry #${attempt}` : 'Implementation',
190
- phase: 'implementing',
191
- attempt,
192
- executionId: data.executionId,
193
- startTime: new Date().toISOString(),
194
- });
195
- await this.saveWorkflow(wf);
196
- // Notify listeners that an execution for this workflow has started
197
- this.emit('workflowExecutionStarted', {
198
- workflowId: wfId,
199
- executionId: data.executionId,
200
- phase: 'implementing',
201
- workflow: wf,
202
- });
203
- }
204
- catch (err) {
205
- log.warn('Failed to handle executionCreated event', err, 'vibing-orchestrator');
206
- }
207
- });
208
- }
209
- /**
210
- * Initialize workflow orchestrator by loading existing workflows
211
- * Call this after construction to load persisted workflows
212
- */
213
- async initialize() {
214
- try {
215
- // Load existing workflows from database
216
- const all = await this.workflowDB.getAll();
217
- const map = new Map();
218
- for (const [id, wf] of Object.entries(all)) {
219
- map.set(id, this.normalizeWorkflow(wf));
220
- }
221
- this.workflows = map;
222
- log.debug('Loaded existing workflows from disk', { count: this.workflows.size }, 'vibing-orchestrator');
223
- log.debug('Vibing orchestrator initialized with persisted workflows', undefined, 'vibing-orchestrator');
224
- // Start reliability components
225
- this.reconciler.start();
226
- this.idempotencyGuard.start();
227
- log.info('Started workflow reconciler and idempotency guard', {}, 'vibing-orchestrator');
228
- }
229
- catch (error) {
230
- log.warn('Failed to load workflows from disk', error, 'vibing-orchestrator');
231
- }
232
- this.initialized = true;
233
- }
234
- /**
235
- * Shutdown the orchestrator and cleanup resources
236
- */
237
- async shutdown() {
238
- this.reconciler.stop();
239
- this.idempotencyGuard.stop();
240
- this.heartbeatTracker.clear();
241
- log.info('Vibing orchestrator shutdown complete', {}, 'vibing-orchestrator');
242
- }
243
- // ============================================================================
244
- // PHASE 3: Reliability & Observability Helpers
245
- // ============================================================================
246
- /**
247
- * Get workflow snapshots for reconciler
248
- */
249
- getWorkflowSnapshots() {
250
- const snapshots = [];
251
- for (const [id, wf] of this.workflows) {
252
- snapshots.push({
253
- id,
254
- taskId: wf.taskId,
255
- phase: wf.phase,
256
- lastUpdatedAt: wf.lastUpdatedAt || wf.startTime,
257
- terminalStatus: wf.phase === 'failed' ? 'failed' : wf.phase === 'paused' ? 'paused' : null,
258
- lastHeartbeat: this.heartbeatTracker.getLastHeartbeat(id),
259
- reconcileAttempts: wf.reconcileAttempts,
260
- });
261
- }
262
- return snapshots;
263
- }
264
- /**
265
- * Handle stuck workflow detected by reconciler
266
- */
267
- async handleStuckWorkflow(workflowId, action) {
268
- const wf = this.workflows.get(workflowId);
269
- if (!wf)
270
- return;
271
- log.warn('Handling stuck workflow', { workflowId, action: action.action, reason: action.reason }, 'vibing-orchestrator');
272
- // Track reconcile attempts
273
- wf.reconcileAttempts = (wf.reconcileAttempts || 0) + 1;
274
- switch (action.action) {
275
- case 'timeout':
276
- // Emit timeout event for UI notification
277
- this.emit('workflowTimeout', {
278
- workflowId,
279
- phase: wf.phase,
280
- reason: action.reason,
281
- timestamp: action.timestamp,
282
- });
283
- break;
284
- case 'fail':
285
- // Mark workflow as failed after max attempts
286
- await this.dispatch(workflowId, { type: 'PAUSE' });
287
- wf.error = action.reason;
288
- await this.saveWorkflow(wf);
289
- this.emit('workflowStuckFailed', {
290
- workflowId,
291
- reason: action.reason,
292
- timestamp: action.timestamp,
293
- });
294
- break;
295
- case 'retry':
296
- // Attempt to retry the current phase
297
- await this.dispatch(workflowId, { type: 'RETRY' });
298
- break;
299
- default:
300
- break;
301
- }
302
- }
303
- /**
304
- * Record heartbeat for active workflow execution
305
- */
306
- recordHeartbeat(workflowId) {
307
- const wf = this.workflows.get(workflowId);
308
- if (wf) {
309
- this.heartbeatTracker.recordHeartbeat(workflowId, wf.phase);
310
- }
311
- }
312
- /**
313
- * Check if reconciler is running
314
- */
315
- isReconcilerRunning() {
316
- return this.reconciler.isRunning();
317
- }
318
- /**
319
- * Get reconcile history for debugging
320
- */
321
- getReconcileHistory() {
322
- return this.reconciler.getReconcileHistory();
323
- }
324
- // Normalize workflow shape so timeline/status writes don't throw
325
- normalizeWorkflow(wf) {
326
- const out = { ...wf };
327
- if (!Array.isArray(out.phaseHistory))
328
- out.phaseHistory = [];
329
- if (!Array.isArray(out.timeline))
330
- out.timeline = [];
331
- if (!out.executionIds || typeof out.executionIds !== 'object')
332
- out.executionIds = {};
333
- const execPhases = ['implementing', 'validating', 'ai-reviewing', 'merging'];
334
- for (const ph of execPhases) {
335
- const arr = out.executionIds[ph];
336
- if (!Array.isArray(arr))
337
- out.executionIds[ph] = [];
338
- }
339
- if (!out.attempts || typeof out.attempts !== 'object')
340
- out.attempts = {};
341
- const metrics = (out.metrics ?? (out.metrics = { durationsMs: {} }));
342
- if (!metrics.durationsMs)
343
- metrics.durationsMs = {};
344
- if (!metrics.lastPhaseStartedAt)
345
- metrics.lastPhaseStartedAt = out.startTime;
346
- if (!out.lastUpdatedAt)
347
- out.lastUpdatedAt = out.startTime;
348
- if (!out.status && out.phase === 'draft')
349
- out.status = 'draft';
350
- return out;
351
- }
352
- // ============================================================================
353
- // NEW STATE MACHINE: Dispatch & Effect Execution
354
- // ============================================================================
355
- /**
356
- * Convert VibingExecution to WorkflowState for the reducer
357
- */
358
- toWorkflowState(wf) {
359
- // Map legacy phases to new simplified phases
360
- // Key insight: checkpoint statuses (implemented, validated, etc.) indicate
361
- // the PREVIOUS phase completed, so they map to the NEXT phase
362
- const phaseMap = {
363
- draft: 'draft',
364
- implementing: 'implementing',
365
- implemented: 'validating', // implementation done -> ready for validation
366
- validating: 'validating',
367
- validated: 'approved', // validation done -> ready for approval/review
368
- 'ai-reviewing': 'approved',
369
- 'ai-reviewed': 'approved',
370
- 'awaiting-review': 'approved',
371
- approved: 'merging', // approved -> ready for merge
372
- merging: 'merging',
373
- merged: 'done', // merged -> ready for cleanup/done
374
- cleaning: 'done',
375
- cleaned: 'done',
376
- completed: 'done',
377
- };
378
- const terminalMap = {
379
- failed: 'failed',
380
- paused: 'paused',
381
- };
382
- return {
383
- id: wf.id,
384
- taskId: wf.taskId,
385
- phase: phaseMap[wf.phase] || 'draft',
386
- terminalStatus: terminalMap[wf.phase] || null,
387
- startTime: wf.startTime,
388
- endTime: wf.endTime,
389
- lastUpdatedAt: wf.lastUpdatedAt || wf.startTime,
390
- attempts: (wf.attempts || {}),
391
- executionIds: (wf.executionIds || {}),
392
- qualityResults: wf.qualityResults,
393
- aiReviewResult: wf.aiReviewResult,
394
- links: wf.links || {},
395
- error: wf.error,
396
- failureContext: wf.failureContext
397
- ? {
398
- atPhase: phaseMap[wf.failureContext.atPhase] || 'implementing',
399
- error: wf.failureContext.error,
400
- timestamp: wf.failureContext.timestamp,
401
- }
402
- : undefined,
403
- config: {
404
- autoQualityChecks: wf.metadata.autoQualityChecks,
405
- requireHumanApproval: wf.metadata.requireHumanApproval,
406
- aiCodeReview: wf.metadata.aiCodeReview ?? true,
407
- stepByStepMode: wf.metadata.stepByStepMode ?? false,
408
- autoCommit: wf.metadata.autoCommit ?? false,
409
- createPR: wf.metadata.createPR,
410
- autoMerge: wf.metadata.autoMerge,
411
- retryPolicy: wf.metadata.retryPolicy || { maxImplementationAttempts: 3 },
412
- },
413
- rerunContext: wf.rerunContextHistory?.[wf.rerunContextHistory.length - 1]
414
- ? {
415
- reason: wf.rerunContextHistory[wf.rerunContextHistory.length - 1].reason,
416
- previousExecutionId: wf.rerunContextHistory[wf.rerunContextHistory.length - 1].previousExecutionId,
417
- }
418
- : undefined,
419
- };
420
- }
421
- /**
422
- * Apply WorkflowState changes back to VibingExecution (for backward compatibility)
423
- */
424
- applyStateToExecution(wf, state) {
425
- const reversePhaseMap = {
426
- draft: 'draft',
427
- implementing: 'implementing',
428
- validating: 'validating',
429
- approved: 'awaiting-review',
430
- merging: 'merging',
431
- done: 'completed',
432
- };
433
- wf.lastUpdatedAt = state.lastUpdatedAt;
434
- wf.error = state.error;
435
- wf.endTime = state.endTime;
436
- if (state.terminalStatus) {
437
- wf.phase = state.terminalStatus;
438
- wf.status = state.terminalStatus;
439
- }
440
- else {
441
- wf.phase = reversePhaseMap[state.phase];
442
- wf.status = reversePhaseMap[state.phase];
443
- }
444
- if (state.qualityResults) {
445
- wf.qualityResults = state.qualityResults;
446
- }
447
- if (state.aiReviewResult) {
448
- wf.aiReviewResult = state.aiReviewResult;
449
- }
450
- if (state.links) {
451
- wf.links = { ...wf.links, ...state.links };
452
- }
453
- if (state.failureContext) {
454
- wf.failureContext = {
455
- atPhase: state.failureContext.atPhase,
456
- error: state.failureContext.error,
457
- timestamp: state.failureContext.timestamp,
458
- };
459
- }
460
- }
461
- /**
462
- * Create an EffectContext that binds orchestrator methods
463
- */
464
- createEffectContext(workflowId, state) {
465
- const wf = this.workflows.get(workflowId);
466
- return {
467
- workflowId,
468
- taskId: state.taskId,
469
- state,
470
- executeImplementation: async (feedback) => {
471
- await this.executeImplementation(workflowId, feedback);
472
- },
473
- executeValidation: async () => {
474
- await this.executeValidation(workflowId);
475
- },
476
- executeAiReview: async () => {
477
- await this.executeAiReviewPhase(workflowId);
478
- },
479
- executeMerge: async () => {
480
- await this.executeMerge(workflowId);
481
- },
482
- executeCleanup: async () => {
483
- await this.executeCleanup(workflowId);
484
- },
485
- stopExecution: async () => {
486
- if (wf) {
487
- const executions = this.agentService.getTaskExecutions(wf.taskId) || [];
488
- const running = executions.find((e) => e.status === 'running');
489
- if (running?.id) {
490
- await this.agentService.stopExecution(running.id);
491
- }
492
- }
493
- },
494
- saveWorkflow: async (updatedState) => {
495
- if (wf) {
496
- this.applyStateToExecution(wf, updatedState);
497
- await this.saveWorkflow(wf);
498
- }
499
- },
500
- updateTask: async () => {
501
- if (wf) {
502
- await this.updateTaskForReviewReadiness(wf);
503
- }
504
- },
505
- emit: (name, payload) => {
506
- this.emit(name, { ...payload, workflow: wf });
507
- },
508
- };
509
- }
510
- /**
511
- * Dispatch an event to the workflow state machine (NEW API)
512
- * This is the new way to trigger state transitions.
513
- * Uses idempotency guard to prevent duplicate event processing.
514
- */
515
- async dispatch(workflowId, event) {
516
- const wf = this.workflows.get(workflowId);
517
- if (!wf) {
518
- throw new Error(`Workflow ${workflowId} not found`);
519
- }
520
- // Generate idempotency key for this event
521
- const eventKey = this.idempotencyGuard.generateEventKey(workflowId, event.type, event);
522
- // Check if this event was already processed
523
- const { skipped } = await this.idempotencyGuard.withIdempotency(eventKey, async () => {
524
- const beforePhase = wf.phase;
525
- const beforeStatus = wf.status;
526
- const timestamp = new Date().toISOString();
527
- log.info('Dispatching workflow event', {
528
- workflowId,
529
- eventType: event.type,
530
- currentPhase: beforePhase,
531
- currentStatus: beforeStatus,
532
- }, 'vibing-orchestrator');
533
- const currentState = this.toWorkflowState(wf);
534
- const { state: newState, effects } = reduce(currentState, event);
535
- // Apply state changes
536
- this.applyStateToExecution(wf, newState);
537
- // Record event in phase history for audit trail
538
- const phaseHistory = wf.phaseHistory || [];
539
- phaseHistory.push({
540
- from: beforePhase,
541
- to: wf.phase,
542
- at: timestamp,
543
- reason: `Event: ${event.type}`,
544
- });
545
- wf.phaseHistory = phaseHistory;
546
- // Record heartbeat on state change
547
- this.heartbeatTracker.recordHeartbeat(workflowId, wf.phase);
548
- // Execute effects
549
- const context = this.createEffectContext(workflowId, newState);
550
- await executeEffects(effects, context);
551
- log.info('Workflow event dispatched', {
552
- workflowId,
553
- eventType: event.type,
554
- beforePhase,
555
- afterPhase: wf.phase,
556
- effectCount: effects.length,
557
- effects: effects.map((e) => e.type),
558
- }, 'vibing-orchestrator');
559
- // Emit a generic event for UI/external listeners
560
- this.emit('workflowEventDispatched', {
561
- workflowId,
562
- event,
563
- beforePhase,
564
- afterPhase: wf.phase,
565
- timestamp,
566
- workflow: wf,
567
- });
568
- });
569
- if (skipped) {
570
- log.debug('Skipped duplicate workflow event', { workflowId, eventType: event.type }, 'vibing-orchestrator');
571
- }
572
- }
573
- /**
574
- * Get the next recommended action for a workflow (uses new state machine)
575
- */
576
- getNextAction(workflowId) {
577
- const wf = this.workflows.get(workflowId);
578
- if (!wf)
579
- return null;
580
- const state = this.toWorkflowState(wf);
581
- return computeNextAction(state);
582
- }
583
- // ============================================================================
584
- // END NEW STATE MACHINE
585
- // ============================================================================
586
- /**
587
- * Execute a task once (no phase management). Optionally associates with a workflow.
588
- */
589
- async executeTask(taskId, workflowId) {
590
- const t = this.taskService.getTask(taskId);
591
- if (!t)
592
- throw new Error(`Task ${taskId} not found`);
593
- // Wait for the executionCreated event for this task/workflow
594
- const createdPromise = this.waitForExecutionCreated(taskId, workflowId, 15000);
595
- // Kick off the agent execution (do not rely on returned id)
596
- // Attach a catch handler to avoid unhandled promise rejection crashing the process
597
- void this.agentService
598
- .executeTask(taskId, workflowId)
599
- .catch((err) => log.error('Agent execution failed to start (executeTask)', err, 'vibing-orchestrator'));
600
- const executionId = await createdPromise;
601
- return { executionId };
602
- }
603
- async stopExecution(executionId) {
604
- await this.agentService.stopExecution(executionId);
605
- }
606
- getExecutionStatus(executionId) {
607
- return this.agentService.getExecutionStatus(executionId);
608
- }
609
- async getPersistedExecutionLogs(executionId) {
610
- return await this.agentService.getPersistedExecutionLogs(executionId);
611
- }
612
- getExecutionLogs(executionId) {
613
- return this.agentService.getExecutionLogs(executionId);
614
- }
615
- getTaskExecutions(taskId) {
616
- return this.agentService.getTaskExecutions(taskId);
617
- }
618
- getExecutionStats() {
619
- return this.agentService.getExecutionStats();
620
- }
621
- async listAllExecutions() {
622
- return await this.agentService.listAllExecutions();
623
- }
624
- async startTaskImprovement(taskId, data, options) {
625
- const task = this.taskService.getTask(taskId);
626
- if (!task)
627
- throw new Error(`Task ${taskId} not found`);
628
- const existingExecutionForTask = this.taskImprovementExecutions.get(taskId);
629
- if (existingExecutionForTask) {
630
- return { executionId: existingExecutionForTask };
631
- }
632
- const requestedExecution = options?.executionId;
633
- if (requestedExecution) {
634
- const status = this.agentService.getExecutionStatus(requestedExecution);
635
- if (status && ['pending', 'running'].includes(status.status)) {
636
- this.taskImprovementExecutions.set(taskId, requestedExecution);
637
- return { executionId: requestedExecution };
638
- }
639
- }
640
- const executionId = requestedExecution || generateId('improve');
641
- if (this.activeImprovements.has(executionId)) {
642
- return { executionId };
643
- }
644
- this.taskImprovementExecutions.set(taskId, executionId);
645
- const job = this.runTaskImprovementPipeline(task, data, executionId, taskId);
646
- this.activeImprovements.set(executionId, job);
647
- void job.finally(() => {
648
- this.activeImprovements.delete(executionId);
649
- const current = this.taskImprovementExecutions.get(taskId);
650
- if (current === executionId) {
651
- this.taskImprovementExecutions.delete(taskId);
652
- }
653
- });
654
- return { executionId };
655
- }
656
- async improveTaskContent(taskId, data, options) {
657
- const task = this.taskService.getTask(taskId);
658
- if (!task)
659
- throw new Error(`Task ${taskId} not found`);
660
- const res = await this.agentService.improveTaskContent(task, data, options?.executionId);
661
- if (options?.workflowId) {
662
- const wf = this.workflows.get(options.workflowId);
663
- if (wf) {
664
- wf.metadata = {
665
- ...wf.metadata,
666
- lastTaskImprovement: { ...res, timestamp: new Date().toISOString() },
667
- };
668
- await this.saveWorkflow(wf);
669
- }
670
- }
671
- return res;
672
- }
673
- async runTaskImprovementPipeline(task, data, executionId, taskId) {
674
- const timestamp = () => new Date().toLocaleTimeString();
675
- const withLog = (message) => {
676
- const baseLogs = this.agentService.getExecutionLogs(executionId) || [];
677
- return [...baseLogs, `[${timestamp()}] ${message}`];
678
- };
679
- const originalSnapshot = JSON.parse(JSON.stringify(task));
680
- let updatedTask = null;
681
- this.agentService.registerCompletionInterceptor(executionId, async () => true);
682
- try {
683
- const improvement = await this.agentService.improveTaskContent(task, data, executionId);
684
- const updates = {
685
- type: improvement.type,
686
- priority: improvement.priority,
687
- content: improvement.content,
688
- };
689
- if (improvement.title) {
690
- updates.title = improvement.title;
691
- }
692
- updatedTask = await this.taskService.updateTask(task.id, updates);
693
- if (!this.gitService) {
694
- throw new Error('Git service unavailable; cannot commit improved task');
695
- }
696
- const taskFilePath = path.join(getVibeDir(), 'tasks', `${updatedTask.id}.md`);
697
- const relativeTaskPath = path.relative(this.gitService.getProjectRoot(), taskFilePath);
698
- const commitMessage = `chore(${updatedTask.id.toLowerCase()}): apply AI improvement`;
699
- let commitSummary = 'Task improvement saved without committing (no changes detected)';
700
- try {
701
- const commitInfo = await this.gitService.commitChanges(updatedTask, commitMessage, [
702
- relativeTaskPath,
703
- ]);
704
- commitSummary = `Task improvement committed (${commitInfo.hash.slice(0, 7)})`;
705
- }
706
- catch (commitError) {
707
- const message = commitError instanceof Error ? commitError.message : String(commitError);
708
- if (/nothing to commit|no changes added to commit|working tree clean/i.test(message)) {
709
- commitSummary = 'No file changes detected; commit skipped';
710
- }
711
- else {
712
- throw new Error(`Failed to commit improved task: ${message}`);
713
- }
714
- }
715
- const completionLogs = withLog('Task file updated with AI improvement').concat(`[${timestamp()}] ${commitSummary}`);
716
- await this.agentService.finalizeExecution(executionId, 'completed', undefined, completionLogs);
717
- }
718
- catch (error) {
719
- if (updatedTask) {
720
- const revertPayload = { ...originalSnapshot };
721
- delete revertPayload.id;
722
- try {
723
- await this.taskService.updateTask(task.id, revertPayload);
724
- }
725
- catch (revertError) {
726
- log.error('Failed to revert task after improvement failure', { taskId, executionId, error: revertError }, 'vibing-orchestrator');
727
- }
728
- }
729
- const message = error instanceof Error ? error.message : String(error);
730
- this.agentService.clearCompletionInterceptor(executionId);
731
- await this.agentService.finalizeExecution(executionId, 'failed', message, withLog(`Error applying task improvement: ${message}`));
732
- log.error('Failed to complete task improvement', { taskId, executionId, error }, 'vibing-orchestrator');
733
- }
734
- finally {
735
- this.agentService.clearCompletionInterceptor(executionId);
736
- }
737
- }
738
- async aiReviewCode(taskId, reviewContext, options) {
739
- const res = await this.agentService.aiReviewCode(taskId, reviewContext, {
740
- overrides: options?.overrides,
741
- workflowId: options?.workflowId,
742
- executionId: options?.executionId,
743
- });
744
- const wfId = options?.workflowId;
745
- if (wfId) {
746
- const wf = this.workflows.get(wfId);
747
- if (wf) {
748
- const timestamp = new Date().toISOString();
749
- wf.metadata = {
750
- ...wf.metadata,
751
- aiReviewResult: { ...res, timestamp },
752
- };
753
- wf.aiReviewResult = { ...res, timestamp };
754
- // Record execution ID under ai-reviewing for UI/observability
755
- if (res?.executionId) {
756
- wf.executionIds = wf.executionIds || {};
757
- const list = wf.executionIds['ai-reviewing'] || [];
758
- wf.executionIds['ai-reviewing'] = [...list, res.executionId];
759
- }
760
- await this.saveWorkflow(wf);
761
- }
762
- }
763
- return res;
764
- }
765
- async aiMerge(taskId, options) {
766
- const overrides = options?.provider || options?.model
767
- ? { provider: options?.provider, model: options?.model }
768
- : undefined;
769
- return await this.agentService.aiMerge(taskId, {
770
- baseBranch: options?.baseBranch,
771
- workflowId: options?.workflowId,
772
- executionId: options?.executionId,
773
- overrides,
774
- });
775
- }
776
- // Worktree façade
777
- async listWorktrees() {
778
- return await this.worktreeService.listWorktrees();
779
- }
780
- async syncWorktreeState() {
781
- await this.worktreeService.syncWorktreeState();
782
- }
783
- getWorktreeInfo(taskId) {
784
- const wt = this.worktreeService.getWorktree(taskId);
785
- if (!wt)
786
- return null;
787
- // Remove non-serializable references before exposing to callers
788
- const { git, ...rest } = wt;
789
- return rest;
790
- }
791
- async createWorktree(taskId) {
792
- const task = this.taskService.getTask(taskId);
793
- if (!task)
794
- throw new Error(`Task ${taskId} not found`);
795
- if (task.deleted_at)
796
- throw new Error(`Task ${taskId} is deleted`);
797
- const wt = await this.worktreeService.createWorktree(task);
798
- const { git, ...rest } = wt;
799
- return rest;
800
- }
801
- async deleteWorktree(taskId, force) {
802
- await this.worktreeService.deleteWorktree(taskId, !!force);
803
- }
804
- async cleanupWorktree(params) {
805
- await this.worktreeService.cleanupWorktree(params);
806
- }
807
- async createPullRequest(taskId, baseBranch = 'main') {
808
- return await this.worktreeService.createPullRequest(taskId, baseBranch);
809
- }
810
- // Open in configured code editor (from settings.development.editor.command)
811
- async openInEditor(worktreePath) {
812
- try {
813
- const settingsService = getSettingsService();
814
- await settingsService.initialize();
815
- const editor = settingsService.getSetting([
816
- 'development',
817
- 'editor',
818
- ]) || { command: 'code', args: [] };
819
- const cmd = editor.command || 'code';
820
- const baseArgs = Array.isArray(editor.args) ? editor.args : [];
821
- spawn(cmd, [...baseArgs, worktreePath], {
822
- stdio: 'ignore',
823
- detached: true,
824
- env: stripNextInjectedEnv(),
825
- });
826
- return { success: true };
827
- }
828
- catch {
829
- // Fallback to cursor if settings service fails
830
- spawn('cursor', [worktreePath], {
831
- stdio: 'ignore',
832
- detached: true,
833
- env: stripNextInjectedEnv(),
834
- });
835
- return { success: true };
836
- }
837
- }
838
- async openInTerminal(worktreePath) {
839
- // TODO: add options to resume the session
840
- try {
841
- const settingsService = getSettingsService();
842
- await settingsService.initialize();
843
- const terminal = settingsService.getSetting([
844
- 'development',
845
- 'terminal',
846
- ]) || {
847
- command: '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal',
848
- args: [],
849
- };
850
- const cmd = terminal.command || '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal';
851
- const baseArgs = Array.isArray(terminal.args) ? terminal.args : [];
852
- spawn(cmd, [...baseArgs, worktreePath], {
853
- stdio: 'ignore',
854
- detached: true,
855
- env: stripNextInjectedEnv(),
856
- });
857
- return { success: true };
858
- }
859
- catch {
860
- // Fallback to default terminal if settings service fails
861
- spawn('/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal', [worktreePath], {
862
- stdio: 'ignore',
863
- detached: true,
864
- env: stripNextInjectedEnv(),
865
- });
866
- return { success: true };
867
- }
868
- }
869
- // Worktree status helpers used by UI gating flows
870
- async checkWorktreeStatus(workflowId) {
871
- const workflow = this.workflows.get(workflowId);
872
- if (!workflow)
873
- throw new Error(`Workflow ${workflowId} not found`);
874
- const worktree = this.getWorktreeInfo(workflow.taskId);
875
- if (!worktree?.path)
876
- throw new Error('No worktree found for this task');
877
- const status = await this.worktreeService.getGitService().getGitStatus(worktree.path);
878
- const files = [...status.modified, ...status.staged, ...status.not_added];
879
- return { clean: files.length === 0, uncommittedFiles: files, worktreePath: worktree.path };
880
- }
881
- async approveAndContinueReview(workflowId) {
882
- const status = await this.checkWorktreeStatus(workflowId);
883
- if (!status.clean) {
884
- throw new Error(`Uncommitted changes found. Please commit or stash the following files:\n${status.uncommittedFiles.join('\n')}`);
885
- }
886
- await this.approveWorkflow(workflowId);
887
- }
888
- /**
889
- * Mark workflow as completed by human decision.
890
- * Does not perform merge/cleanup; simply finalizes the workflow and emits completion.
891
- */
892
- async markWorkflowCompleted(workflowId) {
893
- const workflow = this.workflows.get(workflowId);
894
- if (!workflow)
895
- throw new Error(`Workflow ${workflowId} not found`);
896
- // Record that completion was decided by human
897
- workflow.metadata = {
898
- ...workflow.metadata,
899
- humanCompleted: true,
900
- };
901
- // Use dispatch for manual completion
902
- await this.dispatch(workflowId, { type: 'COMPLETE_MANUAL' });
903
- log.info('Workflow marked completed by human', { taskId: workflow.taskId }, 'vibing-orchestrator');
904
- }
905
- async ensureInitialized() {
906
- if (!this.initialized) {
907
- await this.initialize();
908
- }
909
- }
910
- /**
911
- * Start AI-assisted workflow for a task
912
- */
913
- async startWorkflow(taskId, customConfig) {
914
- const task = this.taskService.getTask(taskId);
915
- if (!task) {
916
- throw new Error(`Task ${taskId} not found`);
917
- }
918
- const workflowId = generateId('workflow');
919
- const defaultConfig = await getDefaultConfig();
920
- // Ensure settings-derived defaults take precedence over constructor fallbacks
921
- const finalConfig = { ...this.config, ...defaultConfig, ...customConfig };
922
- const workflow = this.createNewWorkflow(workflowId, taskId, finalConfig);
923
- this.workflows.set(workflowId, workflow);
924
- // Persist the new workflow
925
- await this.saveWorkflow(workflow);
926
- this.emit('workflowStarted', workflow);
927
- log.info('Starting workflow for task', { taskId, workflowId }, 'vibing-orchestrator');
928
- await this.executeImplementation(workflowId);
929
- return workflowId;
930
- }
931
- /**
932
- * Pause a workflow execution
933
- */
934
- async pauseWorkflow(workflowId) {
935
- const workflow = this.workflows.get(workflowId);
936
- if (!workflow) {
937
- throw new Error(`Workflow ${workflowId} not found`);
938
- }
939
- // Stop current execution if running
940
- // Try to stop the most recent running execution for this task
941
- const executions = this.agentService.getTaskExecutions(workflow.taskId) || [];
942
- const running = executions
943
- .filter((e) => e.status === 'running')
944
- .sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())[0];
945
- const latest = running ||
946
- executions.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())[0];
947
- if (latest?.id) {
948
- try {
949
- await this.agentService.stopExecution(latest.id);
950
- }
951
- catch (error) {
952
- log.warn('Failed to stop execution', error, 'vibing-orchestrator');
953
- }
954
- }
955
- // Use dispatch for state transition
956
- await this.dispatch(workflowId, { type: 'PAUSE' });
957
- log.info('Paused workflow', { workflowId }, 'vibing-orchestrator');
958
- }
959
- /**
960
- * Reset a workflow by deleting its record and optionally its worktree.
961
- */
962
- async resetWorkflow(workflowId, options) {
963
- const wf = this.workflows.get(workflowId);
964
- if (!wf)
965
- throw new Error(`Workflow ${workflowId} not found`);
966
- const taskId = wf.taskId;
967
- if (options?.deleteWorktree) {
968
- try {
969
- await this.deleteWorktree(wf.taskId, !!options.force);
970
- }
971
- catch (e) {
972
- log.warn('Failed to delete worktree during reset', e, 'vibing-orchestrator');
973
- }
974
- }
975
- // Remove from memory and persistence
976
- this.workflows.delete(workflowId);
977
- try {
978
- await this.workflowDB.delete(workflowId);
979
- }
980
- catch (e) {
981
- log.warn('Failed to delete workflow persistence', e, 'vibing-orchestrator');
982
- }
983
- if (taskId) {
984
- try {
985
- const removed = await this.removeWorkflowsByTask(taskId);
986
- if (removed > 0) {
987
- log.info('Deleted additional workflows for task during reset', { taskId, removed }, 'vibing-orchestrator');
988
- }
989
- }
990
- catch (e) {
991
- log.warn('Failed to delete all workflows for task on reset', e, 'vibing-orchestrator');
992
- }
993
- }
994
- this.emit('workflowDeleted', { workflowId, taskId: wf.taskId });
995
- }
996
- /**
997
- * Approve workflow changes and proceed to merge
998
- */
999
- async approveWorkflow(workflowId) {
1000
- const workflow = this.requireWorkflow(workflowId);
1001
- this.assertPhase(workflow, 'awaiting-review', 'approve');
1002
- // Close the awaiting-review timeline item if present
1003
- this.closeTimelineItem(workflow, 'awaiting-review');
1004
- // Use dispatch for state transition - reducer handles status update and effects
1005
- await this.dispatch(workflowId, { type: 'APPROVE' });
1006
- log.info('Approved workflow', { workflowId }, 'vibing-orchestrator');
1007
- }
1008
- /**
1009
- * Reject workflow and request changes
1010
- */
1011
- async rejectWorkflow(workflowId, feedback) {
1012
- const workflow = this.requireWorkflow(workflowId);
1013
- this.assertPhase(workflow, 'awaiting-review', 'reject');
1014
- // Close the awaiting-review timeline item if present
1015
- this.closeTimelineItem(workflow, 'awaiting-review');
1016
- // Use dispatch for state transition - reducer handles re-implementation with feedback
1017
- await this.dispatch(workflowId, { type: 'REJECT', feedback });
1018
- log.info('Rejected workflow', { workflowId, feedback }, 'vibing-orchestrator');
1019
- }
1020
- /**
1021
- * Helper to close a timeline item by phase
1022
- */
1023
- closeTimelineItem(workflow, phase) {
1024
- try {
1025
- const tl = (workflow.timeline || []);
1026
- for (let i = tl.length - 1; i >= 0; i--) {
1027
- const t = tl[i];
1028
- if (t.phase === phase && !t.placeholder && !t.endTime) {
1029
- t.endTime = new Date().toISOString();
1030
- break;
1031
- }
1032
- }
1033
- workflow.timeline = this.computeVisibleTimeline(workflow);
1034
- }
1035
- catch (err) {
1036
- log.debug(`Failed to finalize ${phase} timeline item`, err, 'vibing-orchestrator');
1037
- }
1038
- }
1039
- /**
1040
- * Get workflow status
1041
- */
1042
- getWorkflow(workflowId) {
1043
- const wf = this.workflows.get(workflowId) || null;
1044
- if (wf) {
1045
- wf.timeline = this.computeVisibleTimeline(wf);
1046
- }
1047
- return wf;
1048
- }
1049
- /**
1050
- * Get all workflows for a task
1051
- */
1052
- getTaskWorkflows(taskId) {
1053
- return Array.from(this.workflows.values())
1054
- .filter((w) => w.taskId === taskId)
1055
- .map((wf) => {
1056
- wf.timeline = this.computeVisibleTimeline(wf);
1057
- return wf;
1058
- })
1059
- .sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
1060
- }
1061
- /**
1062
- * Get latest workflow for a task, preferring most recently updated.
1063
- * Falls back to newest startTime when lastUpdatedAt is unavailable.
1064
- */
1065
- getLatestWorkflowByTask(taskId) {
1066
- const list = Array.from(this.workflows.values()).filter((w) => w.taskId === taskId);
1067
- if (list.length === 0)
1068
- return null;
1069
- const sorted = list.sort((a, b) => {
1070
- const aTs = new Date(a.lastUpdatedAt || a.startTime || 0).getTime();
1071
- const bTs = new Date(b.lastUpdatedAt || b.startTime || 0).getTime();
1072
- return bTs - aTs;
1073
- });
1074
- const latest = sorted[0];
1075
- latest.timeline = this.computeVisibleTimeline(latest);
1076
- return latest;
1077
- }
1078
- /**
1079
- * Update UI phase with proper tracking of lastPhase, phaseHistory, and metrics.
1080
- * This is for UI-only phases that don't correspond to FSM states.
1081
- */
1082
- updateUIPhase(workflow, newPhase) {
1083
- const oldPhase = workflow.phase;
1084
- if (oldPhase === newPhase)
1085
- return;
1086
- workflow.lastPhase = oldPhase;
1087
- workflow.phase = newPhase;
1088
- const now = new Date().toISOString();
1089
- const history = workflow.phaseHistory || [];
1090
- history.push({ from: oldPhase, to: newPhase, at: now });
1091
- workflow.phaseHistory = history;
1092
- // Metrics accumulation
1093
- const metrics = (workflow.metrics = workflow.metrics || { durationsMs: {} });
1094
- const startTs = metrics.lastPhaseStartedAt
1095
- ? new Date(metrics.lastPhaseStartedAt).getTime()
1096
- : new Date(workflow.startTime).getTime();
1097
- const nowTs = new Date(now).getTime();
1098
- const delta = Math.max(0, nowTs - startTs);
1099
- const prev = metrics.durationsMs?.[oldPhase] || 0;
1100
- metrics.durationsMs = { ...metrics.durationsMs, [oldPhase]: prev + delta };
1101
- metrics.lastPhaseStartedAt = now;
1102
- if (newPhase === 'completed') {
1103
- metrics.totalDurationMs = Object.values(metrics.durationsMs || {}).reduce((a, b) => a + (b || 0), 0);
1104
- }
1105
- workflow.lastUpdatedAt = now;
1106
- workflow.timeline = this.computeVisibleTimeline(workflow);
1107
- this.heartbeatTracker.recordHeartbeat(workflow.id, newPhase);
1108
- this.emit('workflowPhaseChanged', {
1109
- workflowId: workflow.id,
1110
- oldPhase,
1111
- newPhase,
1112
- workflow,
1113
- });
1114
- }
1115
- /**
1116
- * Update task file when workflow becomes ready for review
1117
- */
1118
- async updateTaskForReviewReadiness(workflow) {
1119
- try {
1120
- const context = this.extractTaskUpdateContext(workflow);
1121
- await this.taskUpdater.updateTaskForReviewReadiness(workflow.taskId, context);
1122
- log.info('Task updated for review readiness', { taskId: workflow.taskId, workflowId: workflow.id }, 'vibing-orchestrator');
1123
- }
1124
- catch (error) {
1125
- log.error('Failed to update task for review readiness', { taskId: workflow.taskId, workflowId: workflow.id, error }, 'vibing-orchestrator');
1126
- // Don't throw - task update failure shouldn't block workflow progression
1127
- }
1128
- }
1129
- /**
1130
- * Extract context information for task updates
1131
- */
1132
- extractTaskUpdateContext(workflow) {
1133
- const context = {
1134
- workflowId: workflow.id,
1135
- phase: workflow.phase,
1136
- filesModified: [],
1137
- keyChanges: [],
1138
- testsAdded: workflow.qualityResults?.details?.tests,
1139
- testsPassed: workflow.qualityResults?.details?.tests,
1140
- buildPassed: workflow.qualityResults?.details?.build,
1141
- qualityChecksPassed: workflow.qualityResults?.overall,
1142
- };
1143
- // Extract AI review score if available
1144
- if (workflow.metadata?.aiReviewResult) {
1145
- context.aiReviewScore = workflow.metadata.aiReviewResult.qualityScore;
1146
- }
1147
- // Extract key changes from implementation attempts
1148
- const implementingExecutions = workflow.executionIds?.implementing || [];
1149
- if (implementingExecutions.length > 0) {
1150
- context.keyChanges = [
1151
- `Implementation completed in ${implementingExecutions.length} attempt(s)`,
1152
- 'Core task functionality implemented',
1153
- ];
1154
- }
1155
- // Extract file modification info from worktree if available
1156
- try {
1157
- const worktree = this.agentService.getWorktreeInfo(workflow.taskId);
1158
- if (worktree?.path) {
1159
- context.filesModified = [`Changes made in worktree: ${worktree.branchName}`];
1160
- }
1161
- }
1162
- catch (error) {
1163
- log.debug('Could not extract worktree info for task update', error, 'vibing-orchestrator');
1164
- }
1165
- // Add quality check failures to key changes if any
1166
- if (workflow.qualityResults?.details) {
1167
- const failedCategories = Object.entries(workflow.qualityResults.details)
1168
- .filter(([, passed]) => !passed)
1169
- .map(([name]) => name);
1170
- if (failedCategories.length > 0) {
1171
- context.keyChanges.push(`Quality issues in: ${failedCategories.join(', ')}`);
1172
- }
1173
- }
1174
- return context;
1175
- }
1176
- /**
1177
- * Save workflow to persistence layer
1178
- */
1179
- async saveWorkflow(workflow) {
1180
- try {
1181
- await this.workflowDB.set(workflow.id, serializeVibe(workflow));
1182
- }
1183
- catch (error) {
1184
- log.warn('Failed to persist workflow', error, 'vibing-orchestrator');
1185
- }
1186
- }
1187
- /**
1188
- * Execute implementation phase with AI coding
1189
- */
1190
- async executeImplementation(workflowId, feedback, providerOverride) {
1191
- const workflow = this.workflows.get(workflowId);
1192
- if (!workflow)
1193
- return;
1194
- // Note: FSM has already transitioned to 'implementing' via START/RETRY event
1195
- // We only need to update timeline/metrics here for UI observability
1196
- this.updateUIPhase(workflow, 'implementing');
1197
- try {
1198
- log.info('Implementing task', { taskId: workflow.taskId }, 'vibing-orchestrator');
1199
- // Start task execution with enhanced prompts and workflow context
1200
- // Prepare rerun context if any
1201
- const previousExecId = (workflow.executionIds?.['implementing'] || []).slice(-1)[0];
1202
- const attempt = (workflow.attempts?.implementing || 0) + 1;
1203
- // Build rerun context automatically when available (no need to plumb reason externally)
1204
- const autoReason = feedback ||
1205
- workflow.failureContext?.error ||
1206
- (workflow.failureContext?.atPhase
1207
- ? `Retry after ${workflow.failureContext.atPhase} failure`
1208
- : undefined);
1209
- const aiReviewSource = workflow.aiReviewResult || workflow.metadata?.aiReviewResult;
1210
- const aiReviewContext = aiReviewSource
1211
- ? {
1212
- summary: aiReviewSource.reviewSummary,
1213
- recommendations: aiReviewSource.recommendations,
1214
- score: aiReviewSource.qualityScore,
1215
- }
1216
- : undefined;
1217
- // Prepare to capture executionId from event, then start execution
1218
- const createdPromise = this.waitForExecutionCreated(workflow.taskId, workflowId, 20000);
1219
- // Fire-and-forget; attach catch to prevent unhandled rejection from crashing process
1220
- void this.agentService
1221
- .executeTask(workflow.taskId, workflowId, {
1222
- rerunContext: autoReason
1223
- ? {
1224
- reason: autoReason,
1225
- previousExecutionId: previousExecId,
1226
- failurePhase: workflow.failureContext?.atPhase,
1227
- qualityResults: workflow.qualityResults,
1228
- aiReview: aiReviewContext,
1229
- attempt,
1230
- }
1231
- : undefined,
1232
- providerOverride: providerOverride || workflow.metadata?.aiRoutingOverrides?.execute_task,
1233
- workflowConfig: workflow.metadata,
1234
- })
1235
- .catch((err) => log.error('Agent execution failed to start (implementation phase)', err, 'vibing-orchestrator'));
1236
- const executionId = await createdPromise;
1237
- // Maintain placeholders and wait for execution to complete
1238
- workflow.timeline = this.computeVisibleTimeline(workflow);
1239
- await this.saveWorkflow(workflow);
1240
- // Wait for execution to complete
1241
- await this.waitForExecution(executionId);
1242
- const execution = this.agentService.getExecutionStatus(executionId);
1243
- if (!execution || execution.status === 'failed') {
1244
- throw new Error(`Implementation failed: ${execution?.error || 'Unknown error'}`);
1245
- }
1246
- // Timeline: complete implementation attempt
1247
- const tImpl = workflow.timeline.find((t) => t.executionId === executionId);
1248
- if (tImpl) {
1249
- tImpl.endTime = execution.endTime || new Date().toISOString();
1250
- tImpl.usage = execution.usage;
1251
- workflow.timeline = this.computeVisibleTimeline(workflow);
1252
- await this.saveWorkflow(workflow);
1253
- }
1254
- log.info('Implementation completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
1255
- // Dispatch FSM event - reducer handles transition to 'validating' and triggers EXECUTE_VALIDATION effect
1256
- // In stepByStepMode, the effect is not triggered by reducer, so validation waits for Continue
1257
- // IMPORTANT: Dispatch before updating UI phase, otherwise toWorkflowState maps 'implemented' -> 'validating'
1258
- // which causes IMPL_SUCCESS to be an invalid transition (expecting 'implementing' phase)
1259
- // Note: dispatch already updates the workflow status via applyStateToExecution, so no need to set it again
1260
- await this.dispatch(workflowId, { type: 'IMPL_SUCCESS', executionId });
1261
- if (workflow.metadata.stepByStepMode) {
1262
- log.info('Manual step mode – implemented; waiting for Continue to run validation', { workflowId }, 'vibing-orchestrator');
1263
- }
1264
- // Note: In non-stepByStepMode, executeValidation is triggered by reducer's EXECUTE_VALIDATION effect
1265
- }
1266
- catch (error) {
1267
- await this.handlePhaseFailure(workflowId, 'implementing', error);
1268
- }
1269
- }
1270
- /**
1271
- * Public: re-run implementation phase on demand
1272
- * Uses dispatch(RETRY) to transition FSM and trigger implementation via effect.
1273
- */
1274
- async rerunImplementation(workflowId, feedback, providerOverride) {
1275
- const workflow = this.workflows.get(workflowId);
1276
- if (!workflow)
1277
- throw new Error(`Workflow ${workflowId} not found`);
1278
- // Store provider override for the effect to pick up
1279
- if (providerOverride) {
1280
- workflow.metadata = workflow.metadata || {};
1281
- workflow.metadata.aiRoutingOverrides = {
1282
- ...(workflow.metadata.aiRoutingOverrides || {}),
1283
- execute_task: providerOverride,
1284
- };
1285
- }
1286
- // Store feedback in failureContext for the implementation to use
1287
- if (feedback) {
1288
- workflow.failureContext = {
1289
- atPhase: workflow.phase,
1290
- error: feedback,
1291
- timestamp: new Date().toISOString(),
1292
- };
1293
- }
1294
- // Dispatch RETRY event - reducer handles transition and triggers EXECUTE_IMPLEMENTATION effect
1295
- await this.dispatch(workflowId, { type: 'RETRY' });
1296
- }
1297
- /**
1298
- * Get previous phase, preferring the phase where failure occurred
1299
- */
1300
- getPreviousPhase(workflowId) {
1301
- const wf = this.workflows.get(workflowId);
1302
- if (!wf)
1303
- return null;
1304
- return wf.failureContext?.atPhase || wf.lastPhase || null;
1305
- }
1306
- /**
1307
- * Rerun the phase that led to failure (or lastPhase if available)
1308
- */
1309
- async rerunPreviousPhase(workflowId) {
1310
- const phase = this.getPreviousPhase(workflowId);
1311
- if (!phase)
1312
- throw new Error('No previous phase recorded.');
1313
- await this.rerunPhase(workflowId, phase);
1314
- }
1315
- /**
1316
- * Rerun a specific phase programmatically
1317
- */
1318
- async rerunPhase(workflowId, phase) {
1319
- // Before rerun, reset timeline and results from the selected phase onward
1320
- await this.resetFromPhase(workflowId, phase);
1321
- switch (phase) {
1322
- case 'implementing':
1323
- // Use dispatch(RETRY) to transition FSM and trigger implementation via effect
1324
- await this.dispatch(workflowId, { type: 'RETRY' });
1325
- return;
1326
- case 'validating':
1327
- await this.executeValidation(workflowId);
1328
- return;
1329
- case 'ai-reviewing':
1330
- await this.executeAiReviewPhase(workflowId);
1331
- return;
1332
- case 'awaiting-review':
1333
- await this.executeAwaitingReview(workflowId);
1334
- return;
1335
- case 'merging':
1336
- await this.executeMerge(workflowId);
1337
- return;
1338
- case 'approved':
1339
- // Failure after approval typically means merge failed; try merging again
1340
- await this.executeMerge(workflowId);
1341
- return;
1342
- case 'cleaning':
1343
- await this.executeCleanup(workflowId);
1344
- return;
1345
- default:
1346
- // For other phases, restart implementation as a safe default
1347
- await this.dispatch(workflowId, { type: 'RETRY' });
1348
- return;
1349
- }
1350
- }
1351
- /**
1352
- * Reset workflow state from a given phase onward (inclusive).
1353
- * Clears timeline entries, execution IDs, attempts, and phase results for later phases
1354
- * so that a rerun behaves like a fresh run from that step.
1355
- */
1356
- async resetFromPhase(workflowId, from) {
1357
- const workflow = this.workflows.get(workflowId);
1358
- if (!workflow)
1359
- return;
1360
- const ordered = [
1361
- 'draft',
1362
- 'implementing',
1363
- 'validating',
1364
- 'ai-reviewing',
1365
- 'awaiting-review',
1366
- 'approved',
1367
- 'merging',
1368
- 'merged',
1369
- 'cleaning',
1370
- 'completed',
1371
- ];
1372
- const idx = ordered.indexOf(from);
1373
- if (idx < 0)
1374
- return;
1375
- const toReset = ordered.slice(idx); // inclusive of `from`
1376
- // Clear execution IDs and attempts for affected phases
1377
- workflow.executionIds = workflow.executionIds || {};
1378
- workflow.attempts = workflow.attempts || {};
1379
- for (const ph of toReset) {
1380
- if (workflow.executionIds[ph])
1381
- workflow.executionIds[ph] = [];
1382
- if (workflow.attempts[ph] !== undefined)
1383
- delete workflow.attempts[ph];
1384
- }
1385
- // Trim timeline items from affected phases
1386
- if (Array.isArray(workflow.timeline)) {
1387
- workflow.timeline = workflow.timeline.filter((t) => ordered.indexOf(t.phase) < idx);
1388
- }
1389
- // Trim phase history entries from affected phases so UI does not reconstruct later steps
1390
- if (Array.isArray(workflow.phaseHistory)) {
1391
- workflow.phaseHistory = workflow.phaseHistory.filter((h) => ordered.indexOf(h.to) < idx);
1392
- // Update lastPhase to the last remaining history entry (or 'draft')
1393
- const last = workflow.phaseHistory[workflow.phaseHistory.length - 1];
1394
- workflow.lastPhase = (last ? last.to : 'draft');
1395
- }
1396
- // Clear phase-specific results when rerunning from or before them
1397
- if (idx <= ordered.indexOf('validating')) {
1398
- workflow.qualityResults = undefined;
1399
- }
1400
- if (idx <= ordered.indexOf('ai-reviewing')) {
1401
- if (workflow.metadata) {
1402
- const { aiReviewResult, ...rest } = workflow.metadata || {};
1403
- workflow.metadata = { ...rest };
1404
- }
1405
- }
1406
- // Clear any failure/error context
1407
- workflow.error = undefined;
1408
- workflow.failureContext = undefined;
1409
- // Persist reset before kicking off rerun
1410
- await this.saveWorkflow(workflow);
1411
- // Notify listeners that the workflow snapshot changed (phase change will follow)
1412
- this.emit('workflowUpdated', workflow);
1413
- }
1414
- /**
1415
- * Perform AI code review of implemented changes
1416
- */
1417
- async performAICodeReview(workflowId, providedExecutionId) {
1418
- const workflow = this.workflows.get(workflowId);
1419
- if (!workflow)
1420
- return { passed: false };
1421
- try {
1422
- log.info('Running AI code review', { taskId: workflow.taskId }, 'vibing-orchestrator');
1423
- // Get worktree info for the task
1424
- const worktree = this.agentService.getWorktreeInfo(workflow.taskId);
1425
- if (!worktree) {
1426
- log.warn('No worktree found for AI code review, skipping', undefined, 'vibing-orchestrator');
1427
- return { passed: true, executionId: providedExecutionId };
1428
- }
1429
- // Execute AI code review via agent service
1430
- const review = await this.agentService.aiReviewCode(workflow.taskId, 'Automated AI code review triggered by workflow orchestrator', {
1431
- overrides: workflow.metadata?.aiRoutingOverrides?.ai_codereview,
1432
- executionId: providedExecutionId,
1433
- workflowId,
1434
- });
1435
- // Persist review result into workflow metadata
1436
- const timestamp = new Date().toISOString();
1437
- workflow.metadata = {
1438
- ...workflow.metadata,
1439
- aiReviewResult: { ...review, timestamp },
1440
- };
1441
- workflow.aiReviewResult = { ...review, timestamp };
1442
- // Track execution ID under ai-reviewing for observability
1443
- const executionId = providedExecutionId ?? review?.executionId;
1444
- if (executionId) {
1445
- workflow.executionIds = workflow.executionIds || {};
1446
- const list = workflow.executionIds['ai-reviewing'] || [];
1447
- if (!list.includes(executionId)) {
1448
- workflow.executionIds['ai-reviewing'] = [...list, executionId];
1449
- }
1450
- }
1451
- await this.saveWorkflow(workflow);
1452
- // Gate by minimum score from settings
1453
- const minScore = await this.getAIReviewThreshold();
1454
- if (review.qualityScore < minScore) {
1455
- const msg = `AI code review score too low (${review.qualityScore} < ${minScore})`;
1456
- log.warn('AI code review score too low', { score: review.qualityScore, minScore }, 'vibing-orchestrator');
1457
- workflow.error = msg;
1458
- return { passed: false, executionId };
1459
- }
1460
- log.info('AI code review passed', { score: review.qualityScore }, 'vibing-orchestrator');
1461
- workflow.error = undefined;
1462
- return { passed: true, executionId };
1463
- }
1464
- catch (error) {
1465
- log.warn('AI code review failed', error, 'vibing-orchestrator');
1466
- const msg = error instanceof Error ? error.message : String(error);
1467
- const workflowRef = this.workflows.get(workflowId);
1468
- if (workflowRef) {
1469
- workflowRef.error = `AI code review failed: ${msg}`;
1470
- }
1471
- return { passed: false, executionId: providedExecutionId };
1472
- }
1473
- }
1474
- /**
1475
- * Execute AI review as a dedicated phase
1476
- * Note: 'ai-reviewing' is a UI-only phase for timeline display, not an FSM phase.
1477
- * FSM remains in 'approved' during AI review.
1478
- */
1479
- async executeAiReviewPhase(workflowId) {
1480
- const workflow = this.workflows.get(workflowId);
1481
- if (!workflow)
1482
- return;
1483
- // Update UI phase for timeline display (FSM stays in 'approved')
1484
- this.updateUIPhase(workflow, 'ai-reviewing');
1485
- try {
1486
- const attemptList = workflow.executionIds?.['ai-reviewing'] || [];
1487
- const attempt = attemptList.length + 1;
1488
- const execId = generateId('review');
1489
- workflow.executionIds = workflow.executionIds || {};
1490
- const aiList = workflow.executionIds['ai-reviewing'] || [];
1491
- workflow.executionIds['ai-reviewing'] = [...aiList, execId];
1492
- workflow.timeline = this.buildTimeline(workflow, {
1493
- id: execId,
1494
- label: attempt > 1 ? `AI Review – Retry #${attempt}` : 'AI Review',
1495
- phase: 'ai-reviewing',
1496
- attempt,
1497
- executionId: execId,
1498
- startTime: new Date().toISOString(),
1499
- });
1500
- await this.saveWorkflow(workflow);
1501
- this.emit('workflowExecutionStarted', {
1502
- workflowId,
1503
- executionId: execId,
1504
- phase: 'ai-reviewing',
1505
- workflow,
1506
- });
1507
- const { passed } = await this.performAICodeReview(workflowId, execId);
1508
- const latest = this.workflows.get(workflowId);
1509
- if (latest) {
1510
- const timeline = (latest.timeline || []);
1511
- const item = timeline.find((t) => t.executionId === execId);
1512
- if (item) {
1513
- item.endTime = new Date().toISOString();
1514
- if (!passed) {
1515
- item.reason = latest.error || 'AI review did not pass the quality threshold';
1516
- }
1517
- latest.timeline = this.computeVisibleTimeline(latest);
1518
- await this.saveWorkflow(latest);
1519
- }
1520
- }
1521
- if (!passed) {
1522
- const reason = workflow.error || 'AI review did not pass the quality threshold';
1523
- const retried = await this.maybeRetryImplementation(workflowId, reason, 'ai-reviewing');
1524
- if (!retried) {
1525
- await this.handlePhaseFailure(workflowId, 'ai-reviewing', workflow.error);
1526
- }
1527
- return;
1528
- }
1529
- // AI review passed → set checkpoint
1530
- workflow.status = 'ai-reviewed';
1531
- workflow.lastUpdatedAt = new Date().toISOString();
1532
- workflow.timeline = this.computeVisibleTimeline(workflow);
1533
- await this.saveWorkflow(workflow);
1534
- workflow.status = 'awaiting-review';
1535
- workflow.lastUpdatedAt = new Date().toISOString();
1536
- await this.saveWorkflow(workflow);
1537
- await this.executeAwaitingReview(workflowId);
1538
- const wf = this.workflows.get(workflowId);
1539
- if (wf?.metadata.stepByStepMode) {
1540
- log.info('Manual step mode – AI review passed; waiting for Continue', { workflowId }, 'vibing-orchestrator');
1541
- }
1542
- }
1543
- catch (error) {
1544
- await this.handlePhaseFailure(workflowId, 'ai-reviewing', error);
1545
- }
1546
- }
1547
- async executeAwaitingReview(workflowId) {
1548
- const workflow = this.workflows.get(workflowId);
1549
- if (!workflow)
1550
- return;
1551
- // Update UI phase for timeline display (FSM is in 'approved')
1552
- // 'awaiting-review' is the UI representation of FSM 'approved' phase
1553
- this.updateUIPhase(workflow, 'awaiting-review');
1554
- // Update task file for review readiness
1555
- await this.updateTaskForReviewReadiness(workflow);
1556
- const hasReal = Array.isArray(workflow.timeline)
1557
- ? workflow.timeline.some((t) => t.phase === 'awaiting-review' && !t.placeholder)
1558
- : false;
1559
- if (!hasReal) {
1560
- workflow.timeline = this.buildTimeline(workflow, {
1561
- id: `awaiting-review:${new Date().toISOString()}`,
1562
- label: PHASE_LABELS['awaiting-review'],
1563
- phase: 'awaiting-review',
1564
- startTime: new Date().toISOString(),
1565
- });
1566
- }
1567
- await this.saveWorkflow(workflow);
1568
- }
1569
- /**
1570
- * Execute validation phase with quality checks
1571
- * Note: FSM has already transitioned to 'validating' via IMPL_SUCCESS event
1572
- */
1573
- async executeValidation(workflowId) {
1574
- const workflow = this.workflows.get(workflowId);
1575
- if (!workflow)
1576
- return;
1577
- // Update UI phase for timeline display (FSM is already in 'validating')
1578
- this.updateUIPhase(workflow, 'validating');
1579
- try {
1580
- if (!workflow.metadata.autoQualityChecks) {
1581
- // TODO implement later
1582
- return;
1583
- }
1584
- log.info('Running quality checks', { taskId: workflow.taskId }, 'vibing-orchestrator');
1585
- // Get worktree path for quality checks
1586
- const worktree = this.agentService.getWorktreeInfo(workflow.taskId);
1587
- const workingDirectory = worktree?.path || process.cwd();
1588
- // Start a generic execution for validation to unify observability
1589
- const execId = await this.agentService.startGenericExecution(workflow.taskId, {
1590
- workflowId,
1591
- workingDirectory,
1592
- });
1593
- const qualityOverride = workflow.metadata?.aiRoutingOverrides?.quality_checks;
1594
- try {
1595
- const resolved = await this.agentService.previewProviderForOperation('quality_checks', qualityOverride);
1596
- const providerLabel = resolved.model
1597
- ? `${resolved.provider} (model: ${resolved.model})`
1598
- : resolved.provider;
1599
- await this.agentService.logGenericExecution(execId, `[validation] Using provider: ${providerLabel}`);
1600
- }
1601
- catch (err) {
1602
- log.warn('Failed to resolve quality-check provider', err, 'vibing-orchestrator');
1603
- }
1604
- workflow.executionIds = workflow.executionIds || {};
1605
- const valList = workflow.executionIds['validating'] || [];
1606
- workflow.executionIds['validating'] = [...valList, execId];
1607
- workflow.timeline = this.buildTimeline(workflow, {
1608
- id: `validation:${new Date().toISOString()}`,
1609
- label: 'Validation',
1610
- phase: 'validating',
1611
- startTime: new Date().toISOString(),
1612
- executionId: execId,
1613
- });
1614
- await this.saveWorkflow(workflow);
1615
- // Notify listeners that a validation execution has started
1616
- this.emit('workflowExecutionStarted', {
1617
- workflowId,
1618
- executionId: execId,
1619
- phase: 'validating',
1620
- workflow,
1621
- });
1622
- // Capture quality checks used for this workflow (from settings if not already set)
1623
- try {
1624
- if (!workflow.metadata.qualityChecks || workflow.metadata.qualityChecks.length === 0) {
1625
- const { getSettingsService } = await import('../settings-service.js');
1626
- const settingsService = getSettingsService();
1627
- await settingsService.initialize();
1628
- const settings = settingsService.getSettings();
1629
- if (settings.quality?.checks?.length) {
1630
- workflow.metadata.qualityChecks = settings.quality.checks;
1631
- await this.saveWorkflow(workflow);
1632
- }
1633
- }
1634
- }
1635
- catch (e) {
1636
- log.warn('Failed to get quality checks from settings', e, 'vibing-orchestrator');
1637
- }
1638
- // Run quality pipeline with streaming logs
1639
- const qualityResults = await this.qualityPipeline.runChecksStreaming(workingDirectory, async (line) => {
1640
- try {
1641
- await this.agentService.logGenericExecution(execId, String(line));
1642
- }
1643
- catch (err) {
1644
- log.debug('Failed to stream validation log line', err, 'vibing-orchestrator');
1645
- }
1646
- });
1647
- workflow.qualityResults = qualityResults;
1648
- // Timeline: end validation
1649
- const timeline = workflow.timeline;
1650
- for (let i = timeline.length - 1; i >= 0; i--) {
1651
- const t = timeline[i];
1652
- if (t.phase === 'validating' && !t.endTime) {
1653
- t.endTime = new Date().toISOString();
1654
- break;
1655
- }
1656
- }
1657
- // Mark execution complete (completed even if checks failed; workflow handles failure)
1658
- await this.agentService.completeGenericExecution(execId, 'completed');
1659
- workflow.timeline = this.computeVisibleTimeline(workflow);
1660
- await this.saveWorkflow(workflow);
1661
- if (!qualityResults.overall) {
1662
- log.info('Quality checks failed', { taskId: workflow.taskId }, 'vibing-orchestrator');
1663
- workflow.error = 'Quality checks failed';
1664
- const reason = 'Quality checks failed. Please address the reported issues.';
1665
- const retried = await this.maybeRetryImplementation(workflowId, reason, 'validating');
1666
- if (!retried) {
1667
- await this.handlePhaseFailure(workflowId, 'validating', workflow.error);
1668
- }
1669
- return;
1670
- }
1671
- log.info('Quality checks passed', { taskId: workflow.taskId }, 'vibing-orchestrator');
1672
- // Dispatch FSM event - reducer handles transition to 'approved' and may trigger AI review
1673
- // IMPORTANT: Dispatch before updating UI phase, otherwise toWorkflowState maps 'validated' -> 'approved'
1674
- // which causes VALID_SUCCESS to be an invalid transition (expecting 'validating' phase)
1675
- // Note: dispatch already updates the workflow status via applyStateToExecution, so no need to set it again
1676
- await this.dispatch(workflowId, { type: 'VALID_SUCCESS', results: qualityResults });
1677
- if (workflow.metadata.stepByStepMode) {
1678
- log.info('Manual step mode – waiting for Continue to run AI review', { workflowId }, 'vibing-orchestrator');
1679
- }
1680
- // Note: In non-stepByStepMode with aiCodeReview, executeAiReviewPhase is triggered by reducer's EXECUTE_AI_REVIEW effect
1681
- }
1682
- catch (error) {
1683
- await this.handlePhaseFailure(workflowId, 'validating', error);
1684
- }
1685
- }
1686
- /**
1687
- * Public: run validation phase on demand
1688
- */
1689
- async runValidation(workflowId) {
1690
- const workflow = this.workflows.get(workflowId);
1691
- if (!workflow)
1692
- throw new Error(`Workflow ${workflowId} not found`);
1693
- await this.executeValidation(workflowId);
1694
- }
1695
- /**
1696
- * Execute merge phase
1697
- * Note: FSM has already transitioned to 'merging' via APPROVE event
1698
- */
1699
- async executeMerge(workflowId, providerOverride) {
1700
- const workflow = this.workflows.get(workflowId);
1701
- if (!workflow)
1702
- return;
1703
- // Update UI phase for timeline display (FSM is already in 'merging')
1704
- this.updateUIPhase(workflow, 'merging');
1705
- let executionId;
1706
- try {
1707
- log.info('AI merging changes', { taskId: workflow.taskId }, 'vibing-orchestrator');
1708
- const mergeList = workflow.executionIds?.['merging'] || [];
1709
- const attempt = mergeList.length + 1;
1710
- executionId = generateId('merge');
1711
- const startTime = new Date().toISOString();
1712
- workflow.executionIds = workflow.executionIds || {};
1713
- workflow.executionIds['merging'] = [...mergeList, executionId];
1714
- workflow.timeline = this.buildTimeline(workflow, {
1715
- id: executionId,
1716
- label: attempt > 1 ? `Merge – Retry #${attempt}` : 'Merge',
1717
- phase: 'merging',
1718
- attempt,
1719
- executionId,
1720
- startTime,
1721
- });
1722
- workflow.lastUpdatedAt = startTime;
1723
- await this.saveWorkflow(workflow);
1724
- this.emit('workflowUpdated', workflow);
1725
- this.emit('workflowExecutionStarted', {
1726
- workflowId,
1727
- executionId,
1728
- phase: 'merging',
1729
- workflow,
1730
- });
1731
- await this.agentService.aiMerge(workflow.taskId, {
1732
- baseBranch: 'main',
1733
- executionId,
1734
- workflowId,
1735
- overrides: providerOverride || workflow.metadata?.aiRoutingOverrides?.ai_merge,
1736
- });
1737
- const mergeExec = this.agentService.getExecutionStatus(executionId);
1738
- if (!mergeExec || mergeExec.status !== 'completed') {
1739
- throw new Error(`AI merge failed: ${mergeExec?.error || 'Unknown error'}`);
1740
- }
1741
- // Verify merge actually succeeded by checking git state
1742
- const mergeVerification = await this.verifyMergeSuccess(workflow.taskId, executionId);
1743
- if (!mergeVerification.success) {
1744
- throw new Error(`Merge verification failed: ${mergeVerification.error}`);
1745
- }
1746
- const latest = this.workflows.get(workflowId) || workflow;
1747
- const timeline = (latest.timeline || []);
1748
- const tMerge = timeline.find((t) => t.executionId === executionId);
1749
- if (tMerge) {
1750
- tMerge.endTime = mergeExec.endTime || new Date().toISOString();
1751
- tMerge.usage = mergeExec.usage;
1752
- if (mergeVerification.mergeCommit) {
1753
- tMerge.mergeCommit = mergeVerification.mergeCommit;
1754
- }
1755
- latest.timeline = this.computeVisibleTimeline(latest);
1756
- latest.lastUpdatedAt = new Date().toISOString();
1757
- await this.saveWorkflow(latest);
1758
- }
1759
- // Store merge commit in workflow links
1760
- if (mergeVerification.mergeCommit) {
1761
- workflow.links = workflow.links || {};
1762
- workflow.links.mergeCommit = mergeVerification.mergeCommit;
1763
- }
1764
- await this.taskService.updateTask(workflow.taskId, { status: 'done' });
1765
- // Dispatch FSM event - reducer handles transition to 'done' and triggers cleanup effect
1766
- // IMPORTANT: Dispatch before updating UI phase, otherwise toWorkflowState maps 'merged' -> 'done'
1767
- // which causes MERGE_SUCCESS to be an invalid transition (expecting 'merging' phase)
1768
- // Note: dispatch already updates the workflow status via applyStateToExecution, so no need to set it again
1769
- await this.dispatch(workflowId, { type: 'MERGE_SUCCESS' });
1770
- if (workflow.metadata.stepByStepMode) {
1771
- log.info('Manual step mode – merge completed; waiting for Continue', { workflowId }, 'vibing-orchestrator');
1772
- }
1773
- // Note: In non-stepByStepMode, executeCleanup is triggered by reducer's EXECUTE_CLEANUP effect
1774
- log.info('Merge completed, ready for cleanup', { taskId: workflow.taskId }, 'vibing-orchestrator');
1775
- }
1776
- catch (error) {
1777
- const latest = this.workflows.get(workflowId) || workflow;
1778
- if (executionId && latest) {
1779
- const timeline = (latest.timeline || []);
1780
- const pending = timeline.find((t) => t.executionId === executionId);
1781
- if (pending && !pending.endTime) {
1782
- pending.endTime = new Date().toISOString();
1783
- if (!pending.reason) {
1784
- pending.reason = error instanceof Error ? error.message : String(error);
1785
- }
1786
- latest.timeline = this.computeVisibleTimeline(latest);
1787
- latest.lastUpdatedAt = new Date().toISOString();
1788
- await this.saveWorkflow(latest);
1789
- }
1790
- }
1791
- await this.handlePhaseFailure(workflowId, 'merging', error);
1792
- }
1793
- }
1794
- /**
1795
- * Verify that a merge actually succeeded by checking git state
1796
- */
1797
- async verifyMergeSuccess(taskId, executionId) {
1798
- try {
1799
- const worktree = this.worktreeService.getWorktree(taskId);
1800
- if (!worktree) {
1801
- return { success: false, error: 'No worktree found for task' };
1802
- }
1803
- const gitService = this.worktreeService.getGitService();
1804
- const mainRepoPath = gitService.getProjectRoot();
1805
- // Check 1: Verify we're on the main branch and it's clean
1806
- const { spawn } = await import('child_process');
1807
- const execGit = (args, cwd) => {
1808
- return new Promise((resolve) => {
1809
- const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
1810
- let stdout = '';
1811
- let stderr = '';
1812
- child.stdout?.on('data', (d) => (stdout += d.toString()));
1813
- child.stderr?.on('data', (d) => (stderr += d.toString()));
1814
- child.on('close', (code) => resolve({ stdout: stdout.trim(), exitCode: code ?? 1 }));
1815
- child.on('error', () => resolve({ stdout: '', exitCode: 1 }));
1816
- });
1817
- };
1818
- // Get current branch in main repo
1819
- const branchResult = await execGit(['branch', '--show-current'], mainRepoPath);
1820
- const currentBranch = branchResult.stdout;
1821
- // Get git status
1822
- const statusResult = await execGit(['status', '--porcelain'], mainRepoPath);
1823
- const hasUncommittedChanges = statusResult.stdout.length > 0;
1824
- // Check if the feature branch commits are in the current branch
1825
- const featureBranch = worktree.branchName;
1826
- const logResult = await execGit(['log', '--oneline', '-20', '--grep', featureBranch], mainRepoPath);
1827
- // Also check for recent merge commits
1828
- const mergeLogResult = await execGit(['log', '--oneline', '-5', '--merges'], mainRepoPath);
1829
- // Get the latest commit hash
1830
- const headResult = await execGit(['rev-parse', 'HEAD'], mainRepoPath);
1831
- const latestCommit = headResult.stdout;
1832
- // Parse execution logs to check for MERGE_STATUS
1833
- let aiReportedSuccess = false;
1834
- let aiReportedFailure = false;
1835
- try {
1836
- const execLogs = await this.agentService.getPersistedExecutionLogs(executionId);
1837
- const logText = execLogs.logs.map((l) => l.message || '').join('\n');
1838
- if (logText.includes('MERGE_STATUS: SUCCESS')) {
1839
- aiReportedSuccess = true;
1840
- }
1841
- if (logText.includes('MERGE_STATUS: FAILED') || logText.includes('unable to proceed')) {
1842
- aiReportedFailure = true;
1843
- }
1844
- }
1845
- catch {
1846
- // Ignore log parsing errors
1847
- }
1848
- // Determine success based on multiple signals
1849
- if (aiReportedFailure) {
1850
- return { success: false, error: 'AI reported merge failure' };
1851
- }
1852
- if (hasUncommittedChanges) {
1853
- return { success: false, error: 'Main repo has uncommitted changes after merge' };
1854
- }
1855
- // Check if we're on main/master and feature branch content was merged
1856
- const isOnMainBranch = ['main', 'master'].includes(currentBranch);
1857
- if (!isOnMainBranch && !aiReportedSuccess) {
1858
- return {
1859
- success: false,
1860
- error: `Not on main branch after merge (current: ${currentBranch})`,
1861
- };
1862
- }
1863
- // If AI reported success or we detect merge evidence, consider it successful
1864
- const hasMergeEvidence = mergeLogResult.stdout.includes(featureBranch) ||
1865
- logResult.stdout.length > 0 ||
1866
- aiReportedSuccess;
1867
- if (hasMergeEvidence || aiReportedSuccess) {
1868
- log.info('Merge verification passed', { taskId, currentBranch, latestCommit, aiReportedSuccess }, 'vibing-orchestrator');
1869
- return { success: true, mergeCommit: latestCommit };
1870
- }
1871
- // Fallback: if status is clean and we're on main, accept it
1872
- if (isOnMainBranch && !hasUncommittedChanges) {
1873
- log.info('Merge verification passed (clean state)', { taskId, currentBranch }, 'vibing-orchestrator');
1874
- return { success: true, mergeCommit: latestCommit };
1875
- }
1876
- return {
1877
- success: false,
1878
- error: 'Could not verify merge completion - no merge evidence found',
1879
- };
1880
- }
1881
- catch (error) {
1882
- const msg = error instanceof Error ? error.message : String(error);
1883
- log.error('Merge verification error', { taskId, error: msg }, 'vibing-orchestrator');
1884
- return { success: false, error: `Verification error: ${msg}` };
1885
- }
1886
- }
1887
- /**
1888
- * Public: run merge phase on demand
1889
- * This will attempt merge regardless of current phase.
1890
- */
1891
- async runMerge(workflowId, options) {
1892
- const workflow = this.workflows.get(workflowId);
1893
- if (!workflow)
1894
- throw new Error(`Workflow ${workflowId} not found`);
1895
- await this.executeMerge(workflowId, { provider: options?.provider, model: options?.model });
1896
- }
1897
- /**
1898
- * Execute cleanup phase: remove worktree/branch and finalize workflow
1899
- * Note: FSM has already transitioned to 'done' via MERGE_SUCCESS event
1900
- */
1901
- async executeCleanup(workflowId) {
1902
- const workflow = this.workflows.get(workflowId);
1903
- if (!workflow)
1904
- return;
1905
- // Update UI phase for timeline display
1906
- workflow.status = 'cleaning';
1907
- this.updateUIPhase(workflow, 'cleaning');
1908
- try {
1909
- log.info('Cleaning up resources', { taskId: workflow.taskId }, 'vibing-orchestrator');
1910
- // Remove worktree and local branch if present
1911
- try {
1912
- await this.worktreeService.deleteWorktree(workflow.taskId, true);
1913
- }
1914
- catch (err) {
1915
- log.warn('Cleanup encountered an issue (continuing)', err, 'vibing-orchestrator');
1916
- }
1917
- // Finalize workflow - update UI phases
1918
- workflow.status = 'cleaned';
1919
- workflow.endTime = new Date().toISOString();
1920
- this.updateUIPhase(workflow, 'cleaned');
1921
- // Dispatch FSM event for cleanup completion
1922
- await this.dispatch(workflowId, { type: 'CLEAN_SUCCESS' });
1923
- workflow.status = 'completed';
1924
- this.updateUIPhase(workflow, 'completed');
1925
- await this.saveWorkflow(workflow);
1926
- this.emit('workflowCompleted', workflow);
1927
- log.info('Workflow completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
1928
- }
1929
- catch (error) {
1930
- log.error('Cleanup failed', error, 'vibing-orchestrator');
1931
- workflow.error = error instanceof Error ? error.message : String(error);
1932
- workflow.failureContext = {
1933
- atPhase: 'cleaning',
1934
- error: workflow.error,
1935
- timestamp: new Date().toISOString(),
1936
- };
1937
- // Dispatch failure event
1938
- await this.dispatch(workflowId, {
1939
- type: 'CLEAN_FAIL',
1940
- error: workflow.error,
1941
- });
1942
- workflow.status = 'failed';
1943
- this.updateUIPhase(workflow, 'failed');
1944
- await this.saveWorkflow(workflow);
1945
- this.emit('workflowFailed', workflow);
1946
- }
1947
- }
1948
- /**
1949
- * Public: run cleanup phase on demand
1950
- */
1951
- async runCleanup(workflowId) {
1952
- const workflow = this.workflows.get(workflowId);
1953
- if (!workflow)
1954
- throw new Error(`Workflow ${workflowId} not found`);
1955
- await this.executeCleanup(workflowId);
1956
- }
1957
- /**
1958
- * Public: manually skip to next phase
1959
- */
1960
- async skipToNextPhase(workflowId) {
1961
- const workflow = this.workflows.get(workflowId);
1962
- if (!workflow)
1963
- throw new Error(`Workflow ${workflowId} not found`);
1964
- // If anything is currently running for this task, stop it to move on quickly
1965
- try {
1966
- const executions = this.agentService.getTaskExecutions(workflow.taskId) || [];
1967
- const running = executions
1968
- .filter((e) => e.status === 'running')
1969
- .sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())[0];
1970
- if (running?.id) {
1971
- await this.agentService.stopExecution(running.id);
1972
- }
1973
- }
1974
- catch (error) {
1975
- log.warn('Failed to stop execution before skipping phase', error, 'vibing-orchestrator');
1976
- }
1977
- // Determine the base phase to compute the "next" from.
1978
- // For paused/failed, use the last known operational phase.
1979
- const lastTransitionTo = workflow.phaseHistory?.[workflow.phaseHistory.length - 1]?.to;
1980
- const basePhase = workflow.phase === 'paused'
1981
- ? lastTransitionTo || workflow.lastPhase || 'draft'
1982
- : workflow.phase === 'failed'
1983
- ? (workflow.failureContext?.atPhase ||
1984
- workflow.lastPhase ||
1985
- lastTransitionTo ||
1986
- 'implementing')
1987
- : workflow.phase;
1988
- const ordered = [
1989
- 'draft',
1990
- 'implementing',
1991
- 'validating',
1992
- 'ai-reviewing',
1993
- 'awaiting-review',
1994
- 'approved',
1995
- 'merging',
1996
- 'merged',
1997
- 'cleaning',
1998
- 'completed',
1999
- // 'failed' and 'paused' are non-linear; excluded from the sequence
2000
- ];
2001
- const idx = ordered.indexOf(basePhase);
2002
- const nextPhase = idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : 'completed';
2003
- if (nextPhase && nextPhase !== workflow.phase) {
2004
- log.info('Manually skipping workflow phase', { workflowId, currentPhase: workflow.phase, basePhase, nextPhase }, 'vibing-orchestrator');
2005
- // Update UI phase directly for skipping (FSM dispatch may not support all UI phases)
2006
- this.updateUIPhase(workflow, nextPhase);
2007
- await this.saveWorkflow(workflow);
2008
- // Immediately kick off the next phase where applicable to make skipping faster
2009
- switch (nextPhase) {
2010
- case 'implementing':
2011
- await this.dispatch(workflowId, { type: 'RETRY' });
2012
- break;
2013
- case 'validating':
2014
- await this.executeValidation(workflowId);
2015
- break;
2016
- case 'ai-reviewing':
2017
- await this.executeAiReviewPhase(workflowId);
2018
- break;
2019
- case 'approved':
2020
- case 'merging':
2021
- await this.executeMerge(workflowId);
2022
- break;
2023
- case 'merged':
2024
- case 'cleaning':
2025
- await this.executeCleanup(workflowId);
2026
- break;
2027
- case 'awaiting-review':
2028
- case 'completed':
2029
- default:
2030
- // No immediate action; awaiting-review waits for human approval; completed is terminal
2031
- break;
2032
- }
2033
- }
2034
- }
2035
- /**
2036
- * Wait for an execution to complete
2037
- */
2038
- async waitForExecution(executionId, timeoutMs = 30 * 60 * 1000) {
2039
- const startTime = Date.now();
2040
- let missingStatusWarned = false;
2041
- while (Date.now() - startTime < timeoutMs) {
2042
- const execution = this.agentService.getExecutionStatus(executionId);
2043
- if (!execution) {
2044
- if (!missingStatusWarned) {
2045
- log.debug('Execution status not yet available; waiting for agent to register', { executionId }, 'vibing-orchestrator');
2046
- missingStatusWarned = true;
2047
- }
2048
- await new Promise((resolve) => setTimeout(resolve, 500));
2049
- continue;
2050
- }
2051
- missingStatusWarned = false;
2052
- if (['completed', 'failed', 'cancelled'].includes(execution.status)) {
2053
- return;
2054
- }
2055
- // Wait 5 seconds before checking again
2056
- await new Promise((resolve) => setTimeout(resolve, 5000));
2057
- }
2058
- throw new Error(`Execution ${executionId} timed out after ${timeoutMs}ms`);
2059
- }
2060
- /**
2061
- * Wait for the AgentService to emit an executionCreated event for the given task/workflow
2062
- */
2063
- waitForExecutionCreated(taskId, workflowId, timeoutMs = 15000) {
2064
- return new Promise((resolve, reject) => {
2065
- const onCreated = (data) => {
2066
- try {
2067
- if (data.taskId !== taskId)
2068
- return;
2069
- if (workflowId && data.workflowId !== workflowId)
2070
- return;
2071
- cleanup();
2072
- resolve(data.executionId);
2073
- }
2074
- catch (err) {
2075
- cleanup();
2076
- reject(err instanceof Error ? err : new Error(String(err)));
2077
- }
2078
- };
2079
- const cleanup = () => {
2080
- clearTimeout(timer);
2081
- this.agentService.off('executionCreated', onCreated);
2082
- };
2083
- const timer = setTimeout(() => {
2084
- cleanup();
2085
- reject(new Error('Timed out waiting for executionCreated event'));
2086
- }, timeoutMs);
2087
- this.agentService.on('executionCreated', onCreated);
2088
- });
2089
- }
2090
- /**
2091
- * Get workflow statistics
2092
- */
2093
- getWorkflowStats() {
2094
- const workflows = Array.from(this.workflows.values());
2095
- const stats = {
2096
- total: workflows.length,
2097
- byPhase: {},
2098
- active: 0,
2099
- completed: 0,
2100
- failed: 0,
2101
- };
2102
- // Initialize phase counts
2103
- const phases = [
2104
- 'draft',
2105
- 'implementing',
2106
- 'validating',
2107
- 'ai-reviewing',
2108
- 'awaiting-review',
2109
- 'paused',
2110
- 'approved',
2111
- 'merging',
2112
- 'merged',
2113
- 'cleaning',
2114
- 'completed',
2115
- 'failed',
2116
- ];
2117
- phases.forEach((phase) => {
2118
- stats.byPhase[phase] = 0;
2119
- });
2120
- // Count workflows by phase
2121
- workflows.forEach((workflow) => {
2122
- stats.byPhase[workflow.phase]++;
2123
- if ([
2124
- 'implementing',
2125
- 'validating',
2126
- 'ai-reviewing',
2127
- 'awaiting-review',
2128
- 'approved',
2129
- 'merging',
2130
- 'cleaning',
2131
- ].includes(workflow.phase)) {
2132
- stats.active++;
2133
- }
2134
- else if (['completed', 'merged'].includes(workflow.phase)) {
2135
- stats.completed++;
2136
- }
2137
- else if (workflow.phase === 'failed') {
2138
- stats.failed++;
2139
- }
2140
- });
2141
- return stats;
2142
- }
2143
- /**
2144
- * Get workflow persistence information
2145
- */
2146
- getWorkflowPersistenceInfo() {
2147
- return {
2148
- filePath: this.workflowDB.getFilePath(),
2149
- workflowCount: this.workflows.size,
2150
- };
2151
- }
2152
- /**
2153
- * Force save all workflows to disk
2154
- */
2155
- async saveAllWorkflows() {
2156
- const obj = {};
2157
- for (const [id, wf] of this.workflows)
2158
- obj[id] = serializeVibe(wf);
2159
- await this.workflowDB.setAll(obj);
2160
- }
2161
- /**
2162
- * Reload workflows from disk
2163
- */
2164
- async reloadWorkflows() {
2165
- const all = await this.workflowDB.getAll();
2166
- const map = new Map();
2167
- for (const [id, wf] of Object.entries(all)) {
2168
- map.set(id, this.normalizeWorkflow(wf));
2169
- }
2170
- this.workflows = map;
2171
- log.info('Reloaded workflows from disk', { count: this.workflows.size }, 'vibing-orchestrator');
2172
- }
2173
- /**
2174
- * Remove workflows by task ID
2175
- */
2176
- async removeWorkflowsByTask(taskId) {
2177
- const all = await this.workflowDB.getAll();
2178
- let removedCount = 0;
2179
- for (const [id, workflow] of Object.entries(all)) {
2180
- if (workflow.taskId === taskId) {
2181
- delete all[id];
2182
- removedCount++;
2183
- }
2184
- }
2185
- if (removedCount > 0) {
2186
- await this.workflowDB.setAll(all);
2187
- }
2188
- // Also remove from memory
2189
- for (const [id, workflow] of this.workflows) {
2190
- if (workflow.taskId === taskId) {
2191
- this.workflows.delete(id);
2192
- }
2193
- }
2194
- return removedCount;
2195
- }
2196
- /**
2197
- * Get comprehensive workflow statistics
2198
- */
2199
- async getDetailedStats() {
2200
- return {
2201
- memoryStats: this.getWorkflowStats(),
2202
- persistenceStats: await getDbStats(this.workflowDB),
2203
- persistencePath: this.workflowDB.getFilePath(),
2204
- };
2205
- }
2206
- /**
2207
- * Update workflow options without changing phase
2208
- */
2209
- async updateWorkflowOptions(workflowId, config) {
2210
- const workflow = this.workflows.get(workflowId);
2211
- if (!workflow) {
2212
- throw new Error(`Workflow ${workflowId} not found`);
2213
- }
2214
- // Update metadata with new config
2215
- workflow.metadata = { ...workflow.metadata, ...config };
2216
- // Persist the updated workflow
2217
- await this.saveWorkflow(workflow);
2218
- log.info('Updated workflow options', { workflowId }, 'vibing-orchestrator');
2219
- this.emit('workflowOptionsUpdated', workflow);
2220
- }
2221
- /**
2222
- * Continue workflow execution from current state
2223
- * Uses the new state machine to determine the next action
2224
- */
2225
- async continueWorkflow(workflowId) {
2226
- const workflow = this.workflows.get(workflowId);
2227
- if (!workflow) {
2228
- throw new Error(`Workflow ${workflowId} not found`);
2229
- }
2230
- // Use new state machine to compute next action
2231
- const nextAction = this.getNextAction(workflowId);
2232
- log.info('Continuing workflow (state-machine-driven)', {
2233
- workflowId,
2234
- phase: workflow.phase,
2235
- status: workflow.status,
2236
- nextAction: nextAction?.action,
2237
- }, 'vibing-orchestrator');
2238
- if (!nextAction || nextAction.action === 'none') {
2239
- log.info('No actionable next step', {
2240
- workflowId,
2241
- phase: workflow.phase,
2242
- reason: nextAction?.action === 'none' ? nextAction.reason : 'null action',
2243
- }, 'vibing-orchestrator');
2244
- return;
2245
- }
2246
- switch (nextAction.action) {
2247
- case 'implementing':
2248
- await this.executeImplementation(workflowId, nextAction.feedback);
2249
- return;
2250
- case 'validating':
2251
- await this.executeValidation(workflowId);
2252
- return;
2253
- case 'ai-reviewing':
2254
- await this.executeAiReviewPhase(workflowId);
2255
- return;
2256
- case 'awaiting-approval':
2257
- await this.executeAwaitingReview(workflowId);
2258
- return;
2259
- case 'merging':
2260
- await this.executeMerge(workflowId);
2261
- return;
2262
- case 'cleanup':
2263
- await this.executeCleanup(workflowId);
2264
- return;
2265
- default:
2266
- log.warn('Unknown next action from state machine', { workflowId, action: nextAction.action }, 'vibing-orchestrator');
2267
- }
2268
- }
2269
- // ============ Small helpers to reduce duplication ============
2270
- requireWorkflow(workflowId) {
2271
- const wf = this.workflows.get(workflowId);
2272
- if (!wf)
2273
- throw new Error(`Workflow ${workflowId} not found`);
2274
- return wf;
2275
- }
2276
- async getAIReviewThreshold() {
2277
- try {
2278
- const settingsService = getSettingsService();
2279
- await settingsService.initialize();
2280
- const settings = settingsService.getSettings();
2281
- return settings.agents.judgeAgent.reviewThresholdScore;
2282
- }
2283
- catch (error) {
2284
- log.warn('Failed to load AI review threshold from settings, using default', error, 'vibing-orchestrator');
2285
- return 70; // fallback default
2286
- }
2287
- }
2288
- assertPhase(workflow, expected, action) {
2289
- if (workflow.phase !== expected) {
2290
- throw new Error(`Cannot ${action} workflow in phase ${workflow.phase}; expected ${expected}`);
2291
- }
2292
- }
2293
- createNewWorkflow(workflowId, taskId, config) {
2294
- const now = new Date().toISOString();
2295
- const wf = {
2296
- id: workflowId,
2297
- taskId,
2298
- phase: 'draft',
2299
- startTime: now,
2300
- endTime: undefined,
2301
- status: 'draft',
2302
- // Phase tracking
2303
- lastPhase: undefined,
2304
- phaseHistory: [],
2305
- failureContext: undefined,
2306
- // Execution tracking and attempts per phase
2307
- executionIds: {
2308
- implementing: [],
2309
- validating: [],
2310
- 'ai-reviewing': [],
2311
- merging: [],
2312
- },
2313
- attempts: {},
2314
- // Rerun context audit trail
2315
- rerunContextHistory: [],
2316
- // Quality + links placeholders
2317
- qualityResults: undefined,
2318
- links: {},
2319
- // Error placeholder
2320
- error: undefined,
2321
- // Observability/metrics
2322
- metrics: { durationsMs: {}, lastPhaseStartedAt: now },
2323
- lastUpdatedAt: now,
2324
- // Persisted workflow configuration
2325
- metadata: config,
2326
- // Initial timeline marker
2327
- timeline: [
2328
- {
2329
- id: 'start',
2330
- label: 'Workflow started',
2331
- phase: 'draft',
2332
- startTime: now,
2333
- },
2334
- ],
2335
- };
2336
- // Seed placeholders for the rest of the flow
2337
- wf.timeline = this.computeVisibleTimeline(wf);
2338
- return wf;
2339
- }
2340
- async maybeRetryImplementation(workflowId, reason, _atPhase) {
2341
- const workflow = this.requireWorkflow(workflowId);
2342
- // In step-by-step mode, do not auto-retry; wait for explicit Continue
2343
- if (workflow.metadata.stepByStepMode) {
2344
- log.info('Step-by-step mode: skipping auto-retry of implementation', { workflowId, reason }, 'vibing-orchestrator');
2345
- return false;
2346
- }
2347
- const max = workflow.metadata.retryPolicy?.maxImplementationAttempts || 0;
2348
- const implementingCount = (workflow.phaseHistory || []).filter((h) => h.to === 'implementing').length;
2349
- if (max > 0 && implementingCount < max) {
2350
- // Store reason in failureContext for the implementation to use
2351
- workflow.failureContext = {
2352
- atPhase: workflow.phase,
2353
- error: reason,
2354
- timestamp: new Date().toISOString(),
2355
- };
2356
- // Dispatch RETRY event - reducer handles transition and triggers EXECUTE_IMPLEMENTATION effect
2357
- await this.dispatch(workflowId, { type: 'RETRY' });
2358
- return true;
2359
- }
2360
- return false;
2361
- }
2362
- async handlePhaseFailure(workflowId, atPhase, error) {
2363
- const workflow = this.requireWorkflow(workflowId);
2364
- const msg = error instanceof Error ? error.message : String(error);
2365
- log.error(`${atPhase} failed`, error, 'vibing-orchestrator');
2366
- workflow.error = msg;
2367
- workflow.failureContext = {
2368
- atPhase,
2369
- error: msg,
2370
- timestamp: new Date().toISOString(),
2371
- };
2372
- // Attach reason to last timeline item of this phase
2373
- const wf = this.workflows.get(workflowId);
2374
- if (wf && Array.isArray(wf.timeline)) {
2375
- for (let i = wf.timeline.length - 1; i >= 0; i--) {
2376
- const t = wf.timeline[i];
2377
- if (t.phase === atPhase) {
2378
- t.reason = msg;
2379
- break;
2380
- }
2381
- }
2382
- await this.saveWorkflow(wf);
2383
- }
2384
- // Dispatch appropriate failure event based on phase
2385
- const failEvent = this.getFailEventForPhase(atPhase, msg);
2386
- if (failEvent) {
2387
- await this.dispatch(workflowId, failEvent);
2388
- }
2389
- // Update UI phase
2390
- workflow.status = 'failed';
2391
- this.updateUIPhase(workflow, 'failed');
2392
- await this.saveWorkflow(workflow);
2393
- this.emit('workflowFailed', workflow);
2394
- }
2395
- getFailEventForPhase(phase, error) {
2396
- switch (phase) {
2397
- case 'implementing':
2398
- return { type: 'IMPL_FAIL', error, canRetry: false };
2399
- case 'validating':
2400
- return { type: 'VALID_FAIL', error };
2401
- case 'merging':
2402
- return { type: 'MERGE_FAIL', error };
2403
- case 'cleaning':
2404
- return { type: 'CLEAN_FAIL', error };
2405
- default:
2406
- return null;
2407
- }
2408
- }
2409
- /**
2410
- * Get the agent service instance
2411
- */
2412
- getAgentService() {
2413
- return this.agentService;
2414
- }
2415
- }
2416
- // Helpers local to this module
2417
- function serializeVibe(workflow) {
2418
- return {
2419
- ...workflow,
2420
- startTime: new Date(workflow.startTime).toISOString(),
2421
- endTime: workflow.endTime ? new Date(workflow.endTime).toISOString() : undefined,
2422
- qualityResults: workflow.qualityResults
2423
- ? {
2424
- ...workflow.qualityResults,
2425
- timestamp: new Date(workflow.qualityResults.timestamp).toISOString(),
2426
- }
2427
- : undefined,
2428
- aiReviewResult: workflow.aiReviewResult
2429
- ? {
2430
- ...workflow.aiReviewResult,
2431
- timestamp: workflow.aiReviewResult.timestamp
2432
- ? new Date(workflow.aiReviewResult.timestamp).toISOString()
2433
- : undefined,
2434
- }
2435
- : undefined,
2436
- };
2437
- }
2438
- async function getDbStats(db) {
2439
- const all = await db.getAll();
2440
- const stats = {
2441
- total: 0,
2442
- byPhase: {},
2443
- oldestWorkflow: undefined,
2444
- newestWorkflow: undefined,
2445
- };
2446
- let oldest = null;
2447
- let newest = null;
2448
- for (const wf of Object.values(all)) {
2449
- stats.total += 1;
2450
- stats.byPhase[wf.phase] = (stats.byPhase[wf.phase] || 0) + 1;
2451
- const start = new Date(wf.startTime);
2452
- if (!oldest || start < oldest) {
2453
- oldest = start;
2454
- stats.oldestWorkflow = wf.id;
2455
- }
2456
- if (!newest || start > newest) {
2457
- newest = start;
2458
- stats.newestWorkflow = wf.id;
2459
- }
2460
- }
2461
- return stats;
2462
- }