vibeman 0.0.2 → 0.0.5

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 (198) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/runtime/api/.tsbuildinfo +1 -1
  3. package/dist/runtime/api/agent/agent-service.d.ts +11 -6
  4. package/dist/runtime/api/agent/agent-service.js +97 -29
  5. package/dist/runtime/api/agent/ai-providers/amp-cli-provider.d.ts +38 -0
  6. package/dist/runtime/api/agent/ai-providers/amp-cli-provider.js +268 -0
  7. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.d.ts +2 -0
  8. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +92 -32
  9. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.d.ts +24 -0
  10. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.js +291 -0
  11. package/dist/runtime/api/agent/ai-providers/index.d.ts +3 -3
  12. package/dist/runtime/api/agent/ai-providers/index.js +3 -1
  13. package/dist/runtime/api/agent/ai-providers/types.d.ts +5 -2
  14. package/dist/runtime/api/agent/amp-cli-provider.test.js +99 -0
  15. package/dist/runtime/api/agent/codex-cli-provider.test.js +54 -7
  16. package/dist/runtime/api/agent/prompt-service.js +108 -105
  17. package/dist/runtime/api/agent/prompt-service.test.js +35 -0
  18. package/dist/runtime/api/agent/routing-policy.d.ts +13 -30
  19. package/dist/runtime/api/agent/routing-policy.js +82 -132
  20. package/dist/runtime/api/agent/routing-policy.test.js +63 -0
  21. package/dist/runtime/api/api/routers/ai.d.ts +15 -3
  22. package/dist/runtime/api/api/routers/ai.js +7 -6
  23. package/dist/runtime/api/api/routers/executions.d.ts +3 -8
  24. package/dist/runtime/api/api/routers/executions.js +2 -2
  25. package/dist/runtime/api/api/routers/provider-config.d.ts +34 -0
  26. package/dist/runtime/api/api/routers/settings.d.ts +19 -0
  27. package/dist/runtime/api/api/routers/settings.js +16 -0
  28. package/dist/runtime/api/api/routers/tasks.d.ts +10 -10
  29. package/dist/runtime/api/api/routers/workflows.d.ts +20 -12
  30. package/dist/runtime/api/api/routers/workflows.js +2 -1
  31. package/dist/runtime/api/api/routers/worktrees.d.ts +2 -2
  32. package/dist/runtime/api/api/trpc.d.ts +18 -18
  33. package/dist/runtime/api/lib/local-config.d.ts +94 -4
  34. package/dist/runtime/api/lib/local-config.js +16 -0
  35. package/dist/runtime/api/lib/provider-detection.d.ts +2 -0
  36. package/dist/runtime/api/lib/provider-detection.js +83 -1
  37. package/dist/runtime/api/lib/server/vibeman-info.d.ts +5 -0
  38. package/dist/runtime/api/lib/server/vibeman-info.js +85 -0
  39. package/dist/runtime/api/lib/trpc/server.d.ts +85 -35
  40. package/dist/runtime/api/persistence/execution-log-persistence.d.ts +1 -1
  41. package/dist/runtime/api/persistence/execution-log-persistence.js +19 -3
  42. package/dist/runtime/api/router.d.ts +85 -35
  43. package/dist/runtime/api/settings-service.js +70 -5
  44. package/dist/runtime/api/tasks/task-file-parser.d.ts +1 -0
  45. package/dist/runtime/api/tasks/task-file-parser.js +20 -1
  46. package/dist/runtime/api/tasks/task-updater.d.ts +62 -0
  47. package/dist/runtime/api/tasks/task-updater.js +260 -0
  48. package/dist/runtime/api/tasks/task-updater.test.d.ts +1 -0
  49. package/dist/runtime/api/tasks/task-updater.test.js +303 -0
  50. package/dist/runtime/api/types/index.d.ts +9 -2
  51. package/dist/runtime/api/types/settings.d.ts +29 -5
  52. package/dist/runtime/api/vcs/git-service.d.ts +9 -0
  53. package/dist/runtime/api/vcs/git-service.js +23 -0
  54. package/dist/runtime/api/vcs/worktree-service.d.ts +1 -1
  55. package/dist/runtime/api/vcs/worktree-service.js +22 -10
  56. package/dist/runtime/api/workflows/quality-pipeline.js +2 -1
  57. package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +93 -5
  58. package/dist/runtime/api/workflows/vibing-orchestrator.js +806 -204
  59. package/dist/runtime/api/workflows/workflow-effects.d.ts +45 -0
  60. package/dist/runtime/api/workflows/workflow-effects.js +49 -0
  61. package/dist/runtime/api/workflows/workflow-reconciler.d.ts +65 -0
  62. package/dist/runtime/api/workflows/workflow-reconciler.js +226 -0
  63. package/dist/runtime/api/workflows/workflow-reducer.d.ts +26 -0
  64. package/dist/runtime/api/workflows/workflow-reducer.js +288 -0
  65. package/dist/runtime/api/workflows/workflow-reducer.test.d.ts +1 -0
  66. package/dist/runtime/api/workflows/workflow-reducer.test.js +247 -0
  67. package/dist/runtime/api/workflows/workflow-schema.d.ts +546 -0
  68. package/dist/runtime/api/workflows/workflow-schema.js +256 -0
  69. package/dist/runtime/web/.next/BUILD_ID +1 -1
  70. package/dist/runtime/web/.next/app-build-manifest.json +51 -44
  71. package/dist/runtime/web/.next/app-path-routes-manifest.json +2 -1
  72. package/dist/runtime/web/.next/build-manifest.json +14 -14
  73. package/dist/runtime/web/.next/prerender-manifest.json +10 -10
  74. package/dist/runtime/web/.next/react-loadable-manifest.json +2 -33
  75. package/dist/runtime/web/.next/required-server-files.json +5 -5
  76. package/dist/runtime/web/.next/routes-manifest.json +8 -0
  77. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +1 -0
  78. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -0
  79. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -0
  80. package/dist/runtime/web/.next/server/app/_not-found/page.js +2 -2
  81. package/dist/runtime/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  82. package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  83. package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
  84. package/dist/runtime/web/.next/server/app/_not-found.rsc +12 -12
  85. package/dist/runtime/web/.next/server/app/api/health/route.js +1 -1
  86. package/dist/runtime/web/.next/server/app/api/health/route.js.nft.json +1 -1
  87. package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  88. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +1 -1
  89. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js.nft.json +1 -1
  90. package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
  91. package/dist/runtime/web/.next/server/app/api/upload/route.js +1 -1
  92. package/dist/runtime/web/.next/server/app/api/upload/route.js.nft.json +1 -1
  93. package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
  94. package/dist/runtime/web/.next/server/app/index.html +2 -2
  95. package/dist/runtime/web/.next/server/app/index.rsc +15 -15
  96. package/dist/runtime/web/.next/server/app/page.js +27 -62
  97. package/dist/runtime/web/.next/server/app/page.js.nft.json +1 -1
  98. package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
  99. package/dist/runtime/web/.next/server/app-paths-manifest.json +2 -1
  100. package/dist/runtime/web/.next/server/chunks/210.js +1 -0
  101. package/dist/runtime/web/.next/server/chunks/291.js +18 -0
  102. package/dist/runtime/web/.next/server/chunks/552.js +22 -0
  103. package/dist/runtime/web/.next/server/chunks/780.js +1 -0
  104. package/dist/runtime/web/.next/server/chunks/905.js +6 -0
  105. package/dist/runtime/web/.next/server/chunks/98.js +1 -0
  106. package/dist/runtime/web/.next/server/middleware-build-manifest.js +1 -1
  107. package/dist/runtime/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  108. package/dist/runtime/web/.next/server/pages/404.html +2 -2
  109. package/dist/runtime/web/.next/server/pages/500.html +1 -1
  110. package/dist/runtime/web/.next/server/pages/_app.js +1 -1
  111. package/dist/runtime/web/.next/server/pages/_app.js.nft.json +1 -1
  112. package/dist/runtime/web/.next/server/pages/_document.js +1 -1
  113. package/dist/runtime/web/.next/server/pages/_document.js.nft.json +1 -1
  114. package/dist/runtime/web/.next/server/pages/_error.js +9 -9
  115. package/dist/runtime/web/.next/server/pages/_error.js.nft.json +1 -1
  116. package/dist/runtime/web/.next/server/pages-manifest.json +1 -1
  117. package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
  118. package/dist/runtime/web/.next/server/webpack-runtime.js +1 -1
  119. package/dist/runtime/web/.next/static/LJFZk_8tvKFN_Ee4HqUuM/_buildManifest.js +1 -0
  120. package/dist/runtime/web/.next/static/chunks/05c91ade-7d09b2b280adffd1.js +1 -0
  121. package/dist/runtime/web/.next/static/chunks/201-51bef3fa8c832e2e.js +1 -0
  122. package/dist/runtime/web/.next/static/chunks/524-89747ed9b0294f8a.js +1 -0
  123. package/dist/runtime/web/.next/static/chunks/554-8bec6e9cca6acc67.js +1 -0
  124. package/dist/runtime/web/.next/static/chunks/764.86e9503a69d45a85.js +1 -0
  125. package/dist/runtime/web/.next/static/chunks/{87c73c54-09e1ba5c70e60a51.js → 7ab4dc20-239138e0ae7af24a.js} +1 -1
  126. package/dist/runtime/web/.next/static/chunks/905-342391e3d3a3678f.js +20 -0
  127. package/dist/runtime/web/.next/static/chunks/a8a5ce16-4edea7df2d9b544a.js +79 -0
  128. package/dist/runtime/web/.next/static/chunks/{8bb4d8db-3e2aa02b0a2384b9.js → ad74d572-4c1b162e2c15acaa.js} +1 -1
  129. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-7b752a8641f96c1f.js +1 -0
  130. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-34e66b251c2b5044.js +1 -0
  131. package/dist/runtime/web/.next/static/chunks/app/api/health/route-7b752a8641f96c1f.js +1 -0
  132. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-7b752a8641f96c1f.js +1 -0
  133. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-7b752a8641f96c1f.js +1 -0
  134. package/dist/runtime/web/.next/static/chunks/app/layout-df9ac93cb02b2385.js +1 -0
  135. package/dist/runtime/web/.next/static/chunks/app/page-6610743f7de5f92a.js +1 -0
  136. package/dist/runtime/web/.next/static/chunks/c25e0690-e9b798b8de667da1.js +1 -0
  137. package/dist/runtime/web/.next/static/chunks/framework-57157ec4d37f64aa.js +1 -0
  138. package/dist/runtime/web/.next/static/chunks/main-app-156cc0c60371bd78.js +1 -0
  139. package/dist/runtime/web/.next/static/chunks/main-df25d367c47b1fec.js +1 -0
  140. package/dist/runtime/web/.next/static/chunks/pages/_app-9f629a5e1131d19f.js +1 -0
  141. package/dist/runtime/web/.next/static/chunks/pages/_error-9238238274c7efcd.js +1 -0
  142. package/dist/runtime/web/.next/static/chunks/webpack-cd50e39b423d1808.js +1 -0
  143. package/dist/runtime/web/.next/static/css/4fbf378a264bd4ea.css +1 -0
  144. package/dist/runtime/web/package.json +8 -8
  145. package/dist/runtime/web/server.js +1 -1
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/package.json +3 -37
  148. package/dist/runtime/api/lib/image-paste-drop-extension.d.ts +0 -26
  149. package/dist/runtime/api/lib/image-paste-drop-extension.js +0 -125
  150. package/dist/runtime/api/lib/markdown-utils.d.ts +0 -8
  151. package/dist/runtime/api/lib/markdown-utils.js +0 -282
  152. package/dist/runtime/api/lib/markdown-utils.test.js +0 -348
  153. package/dist/runtime/api/lib/tiptap-utils.clamp-selection.test.js +0 -27
  154. package/dist/runtime/api/lib/tiptap-utils.d.ts +0 -130
  155. package/dist/runtime/api/lib/tiptap-utils.js +0 -327
  156. package/dist/runtime/api/lib/trpc/client.d.ts +0 -1
  157. package/dist/runtime/api/lib/trpc/client.js +0 -5
  158. package/dist/runtime/web/.next/server/chunks/217.js +0 -1
  159. package/dist/runtime/web/.next/server/chunks/383.js +0 -6
  160. package/dist/runtime/web/.next/server/chunks/458.js +0 -1
  161. package/dist/runtime/web/.next/server/chunks/576.js +0 -18
  162. package/dist/runtime/web/.next/server/chunks/635.js +0 -22
  163. package/dist/runtime/web/.next/server/chunks/761.js +0 -1
  164. package/dist/runtime/web/.next/server/chunks/777.js +0 -3
  165. package/dist/runtime/web/.next/server/chunks/825.js +0 -1
  166. package/dist/runtime/web/.next/server/chunks/838.js +0 -1
  167. package/dist/runtime/web/.next/server/chunks/973.js +0 -15
  168. package/dist/runtime/web/.next/static/chunks/18-15c10d3288afef2e.js +0 -1
  169. package/dist/runtime/web/.next/static/chunks/1c0ca389.537bbe362e3ffbd9.js +0 -3
  170. package/dist/runtime/web/.next/static/chunks/22747d63-ad5da0c19f4cfe41.js +0 -71
  171. package/dist/runtime/web/.next/static/chunks/277-0142a939f08738c3.js +0 -63
  172. package/dist/runtime/web/.next/static/chunks/355.056c2645878a799a.js +0 -1
  173. package/dist/runtime/web/.next/static/chunks/420.a5ccf151c9e2b2f1.js +0 -1
  174. package/dist/runtime/web/.next/static/chunks/439.1be0c6242fd248d5.js +0 -15
  175. package/dist/runtime/web/.next/static/chunks/440.c52e7c0f797e22b2.js +0 -1
  176. package/dist/runtime/web/.next/static/chunks/575-e2478287c27da87b.js +0 -1
  177. package/dist/runtime/web/.next/static/chunks/691.920d88c115087314.js +0 -1
  178. package/dist/runtime/web/.next/static/chunks/765-e838910065b50c3d.js +0 -1
  179. package/dist/runtime/web/.next/static/chunks/891cff7f.0f71fc028f87e683.js +0 -1
  180. package/dist/runtime/web/.next/static/chunks/9af238c7-271a911d4e99ab18.js +0 -1
  181. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-1cb74d1cba27d0ab.js +0 -1
  182. package/dist/runtime/web/.next/static/chunks/app/api/health/route-105a61ae865ba536.js +0 -1
  183. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-105a61ae865ba536.js +0 -1
  184. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-105a61ae865ba536.js +0 -1
  185. package/dist/runtime/web/.next/static/chunks/app/layout-8435322f09fd0975.js +0 -1
  186. package/dist/runtime/web/.next/static/chunks/app/page-8c3ba579efc6f918.js +0 -1
  187. package/dist/runtime/web/.next/static/chunks/cac567b0-5b77dd12911823cd.js +0 -1
  188. package/dist/runtime/web/.next/static/chunks/framework-2518f1345b5b2806.js +0 -1
  189. package/dist/runtime/web/.next/static/chunks/main-17665e5e39de9a8a.js +0 -1
  190. package/dist/runtime/web/.next/static/chunks/main-app-c0b0f5ba4f7f9d75.js +0 -1
  191. package/dist/runtime/web/.next/static/chunks/pages/_app-d6f6b3bbc3d81ee1.js +0 -1
  192. package/dist/runtime/web/.next/static/chunks/pages/_error-75a96cf1997cc3b9.js +0 -1
  193. package/dist/runtime/web/.next/static/chunks/webpack-c8de37305b4635cf.js +0 -1
  194. package/dist/runtime/web/.next/static/css/08c950681f1a9a92.css +0 -1
  195. package/dist/runtime/web/.next/static/mRpNgPfbYR_0wrODzlg_4/_buildManifest.js +0 -1
  196. /package/dist/runtime/api/{lib/markdown-utils.test.d.ts → agent/amp-cli-provider.test.d.ts} +0 -0
  197. /package/dist/runtime/api/{lib/tiptap-utils.clamp-selection.test.d.ts → agent/routing-policy.test.d.ts} +0 -0
  198. /package/dist/runtime/web/.next/static/{mRpNgPfbYR_0wrODzlg_4 → LJFZk_8tvKFN_Ee4HqUuM}/_ssgManifest.js +0 -0
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
+ import { TaskUpdater } from '../tasks/task-updater.js';
2
3
  import { QualityPipeline } from './quality-pipeline.js';
3
4
  import { DatabaseService } from '../persistence/database-service.js';
4
5
  import { log } from '../lib/logger.js';
@@ -8,35 +9,20 @@ import { stripNextInjectedEnv } from '../utils/stripNextEnv.js';
8
9
  import { spawn } from 'child_process';
9
10
  import { getSettingsService } from '../settings-service.js';
10
11
  import { getVibeDir } from '../lib/server/project-root.js';
11
- // Canonical phase flow and labels for server-driven timeline and placeholders
12
- // Action phases only; placeholders are created for these
13
- const PHASE_FLOW = [
14
- 'implementing',
15
- 'validating',
16
- 'ai-reviewing',
17
- // Show awaiting-review explicitly in the main flow so the timeline
18
- // reflects the human approval step in order
19
- 'awaiting-review',
20
- 'merging',
21
- 'cleaning',
22
- ];
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
23
19
  const PHASE_LABELS = {
20
+ ...EXTENDED_PHASE_LABELS,
24
21
  draft: 'Workflow started',
25
22
  implementing: 'Implementation',
26
- implemented: 'Implemented',
27
23
  validating: 'Validation',
28
- validated: 'Validated',
29
- 'ai-reviewing': 'AI Review',
30
- 'ai-reviewed': 'AI Reviewed',
31
- 'awaiting-review': 'Awaiting Review',
32
- paused: 'Paused',
33
- approved: 'Approved',
34
24
  merging: 'Merge',
35
- merged: 'Merged',
36
- cleaning: 'Cleaning',
37
- cleaned: 'Cleaned',
38
- completed: 'Completed',
39
- failed: 'Failed',
25
+ approved: 'Approved',
40
26
  };
41
27
  // Default config
42
28
  const getDefaultConfig = async () => {
@@ -160,12 +146,23 @@ export class VibingOrchestrator extends EventEmitter {
160
146
  this.gitService = gitService;
161
147
  this.workflows = new Map();
162
148
  this.initialized = false;
149
+ this.activeImprovements = new Map();
150
+ this.taskImprovementExecutions = new Map();
163
151
  this.qualityPipeline = new QualityPipeline();
152
+ this.taskUpdater = new TaskUpdater();
164
153
  // Persist workflows in a JSON file using the generic database service
165
154
  const filePath = persistenceDataDir
166
155
  ? path.join(persistenceDataDir, 'workflows.json')
167
- : path.join(getVibeDir(), 'workflows.json');
156
+ : path.join(getVibeDir(), '.local/workflows.json');
168
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));
169
166
  // Forward execution updates from AgentService so UIs can subscribe via orchestrator
170
167
  this.agentService.on('executionUpdated', (update) => {
171
168
  this.emit('executionUpdated', update);
@@ -224,12 +221,106 @@ export class VibingOrchestrator extends EventEmitter {
224
221
  this.workflows = map;
225
222
  log.debug('Loaded existing workflows from disk', { count: this.workflows.size }, 'vibing-orchestrator');
226
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');
227
228
  }
228
229
  catch (error) {
229
230
  log.warn('Failed to load workflows from disk', error, 'vibing-orchestrator');
230
231
  }
231
232
  this.initialized = true;
232
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
+ }
233
324
  // Normalize workflow shape so timeline/status writes don't throw
234
325
  normalizeWorkflow(wf) {
235
326
  const out = { ...wf };
@@ -258,6 +349,240 @@ export class VibingOrchestrator extends EventEmitter {
258
349
  out.status = 'draft';
259
350
  return out;
260
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
+ // ============================================================================
261
586
  /**
262
587
  * Execute a task once (no phase management). Optionally associates with a workflow.
263
588
  */
@@ -296,6 +621,38 @@ export class VibingOrchestrator extends EventEmitter {
296
621
  async listAllExecutions() {
297
622
  return await this.agentService.listAllExecutions();
298
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
+ }
299
656
  async improveTaskContent(taskId, data, options) {
300
657
  const task = this.taskService.getTask(taskId);
301
658
  if (!task)
@@ -313,6 +670,71 @@ export class VibingOrchestrator extends EventEmitter {
313
670
  }
314
671
  return res;
315
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
+ }
316
738
  async aiReviewCode(taskId, reviewContext, options) {
317
739
  const res = await this.agentService.aiReviewCode(taskId, reviewContext, {
318
740
  overrides: options?.overrides,
@@ -323,10 +745,12 @@ export class VibingOrchestrator extends EventEmitter {
323
745
  if (wfId) {
324
746
  const wf = this.workflows.get(wfId);
325
747
  if (wf) {
748
+ const timestamp = new Date().toISOString();
326
749
  wf.metadata = {
327
750
  ...wf.metadata,
328
- aiReviewResult: { ...res, timestamp: new Date().toISOString() },
751
+ aiReviewResult: { ...res, timestamp },
329
752
  };
753
+ wf.aiReviewResult = { ...res, timestamp };
330
754
  // Record execution ID under ai-reviewing for UI/observability
331
755
  if (res?.executionId) {
332
756
  wf.executionIds = wf.executionIds || {};
@@ -474,11 +898,9 @@ export class VibingOrchestrator extends EventEmitter {
474
898
  ...workflow.metadata,
475
899
  humanCompleted: true,
476
900
  };
477
- workflow.endTime = new Date().toISOString();
478
- await this.transitionToPhase(workflowId, 'completed');
479
- this.emit('workflowCompleted', workflow);
901
+ // Use dispatch for manual completion
902
+ await this.dispatch(workflowId, { type: 'COMPLETE_MANUAL' });
480
903
  log.info('Workflow marked completed by human', { taskId: workflow.taskId }, 'vibing-orchestrator');
481
- await this.saveWorkflow(workflow);
482
904
  }
483
905
  async ensureInitialized() {
484
906
  if (!this.initialized) {
@@ -530,12 +952,8 @@ export class VibingOrchestrator extends EventEmitter {
530
952
  log.warn('Failed to stop execution', error, 'vibing-orchestrator');
531
953
  }
532
954
  }
533
- workflow.phase = 'paused';
534
- workflow.error = 'Paused by user';
535
- workflow.status = 'paused';
536
- // Persist the workflow state
537
- await this.saveWorkflow(workflow);
538
- this.emit('workflowPaused', workflow);
955
+ // Use dispatch for state transition
956
+ await this.dispatch(workflowId, { type: 'PAUSE' });
539
957
  log.info('Paused workflow', { workflowId }, 'vibing-orchestrator');
540
958
  }
541
959
  /**
@@ -582,31 +1000,10 @@ export class VibingOrchestrator extends EventEmitter {
582
1000
  const workflow = this.requireWorkflow(workflowId);
583
1001
  this.assertPhase(workflow, 'awaiting-review', 'approve');
584
1002
  // Close the awaiting-review timeline item if present
585
- try {
586
- const tl = (workflow.timeline || []);
587
- for (let i = tl.length - 1; i >= 0; i--) {
588
- const t = tl[i];
589
- if (t.phase === 'awaiting-review' && !t.placeholder && !t.endTime) {
590
- t.endTime = new Date().toISOString();
591
- break;
592
- }
593
- }
594
- workflow.timeline = this.computeVisibleTimeline(workflow);
595
- await this.saveWorkflow(workflow);
596
- }
597
- catch (err) {
598
- log.debug('Failed to finalize awaiting-review timeline item', err, 'vibing-orchestrator');
599
- }
600
- workflow.status = 'approved';
601
- workflow.lastUpdatedAt = new Date().toISOString();
602
- await this.transitionToPhase(workflowId, 'approved');
603
- await this.saveWorkflow(workflow);
604
- if (workflow.metadata.stepByStepMode) {
605
- log.info('Manual step mode – approved; waiting for Continue to run merge', { workflowId }, 'vibing-orchestrator');
606
- }
607
- else {
608
- await this.executeMerge(workflowId);
609
- }
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');
610
1007
  }
611
1008
  /**
612
1009
  * Reject workflow and request changes
@@ -615,24 +1012,29 @@ export class VibingOrchestrator extends EventEmitter {
615
1012
  const workflow = this.requireWorkflow(workflowId);
616
1013
  this.assertPhase(workflow, 'awaiting-review', 'reject');
617
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) {
618
1024
  try {
619
1025
  const tl = (workflow.timeline || []);
620
1026
  for (let i = tl.length - 1; i >= 0; i--) {
621
1027
  const t = tl[i];
622
- if (t.phase === 'awaiting-review' && !t.placeholder && !t.endTime) {
1028
+ if (t.phase === phase && !t.placeholder && !t.endTime) {
623
1029
  t.endTime = new Date().toISOString();
624
1030
  break;
625
1031
  }
626
1032
  }
627
1033
  workflow.timeline = this.computeVisibleTimeline(workflow);
628
- await this.saveWorkflow(workflow);
629
1034
  }
630
1035
  catch (err) {
631
- log.debug('Failed to finalize awaiting-review timeline item', err, 'vibing-orchestrator');
1036
+ log.debug(`Failed to finalize ${phase} timeline item`, err, 'vibing-orchestrator');
632
1037
  }
633
- // Return to implementation phase with feedback
634
- await this.transitionToPhase(workflowId, 'implementing');
635
- await this.executeImplementation(workflowId, feedback);
636
1038
  }
637
1039
  /**
638
1040
  * Get workflow status
@@ -674,17 +1076,17 @@ export class VibingOrchestrator extends EventEmitter {
674
1076
  return latest;
675
1077
  }
676
1078
  /**
677
- * Transition workflow to new phase
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.
678
1081
  */
679
- async transitionToPhase(workflowId, newPhase) {
680
- const workflow = this.workflows.get(workflowId);
681
- if (!workflow)
682
- return;
1082
+ updateUIPhase(workflow, newPhase) {
683
1083
  const oldPhase = workflow.phase;
1084
+ if (oldPhase === newPhase)
1085
+ return;
684
1086
  workflow.lastPhase = oldPhase;
685
1087
  workflow.phase = newPhase;
686
- const history = workflow.phaseHistory || [];
687
1088
  const now = new Date().toISOString();
1089
+ const history = workflow.phaseHistory || [];
688
1090
  history.push({ from: oldPhase, to: newPhase, at: now });
689
1091
  workflow.phaseHistory = history;
690
1092
  // Metrics accumulation
@@ -702,9 +1104,74 @@ export class VibingOrchestrator extends EventEmitter {
702
1104
  }
703
1105
  workflow.lastUpdatedAt = now;
704
1106
  workflow.timeline = this.computeVisibleTimeline(workflow);
705
- log.info('Workflow phase transition', { workflowId, oldPhase, newPhase }, 'vibing-orchestrator');
706
- await this.saveWorkflow(workflow);
707
- this.emit('workflowPhaseChanged', { workflowId, oldPhase, newPhase, 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;
708
1175
  }
709
1176
  /**
710
1177
  * Save workflow to persistence layer
@@ -724,7 +1191,9 @@ export class VibingOrchestrator extends EventEmitter {
724
1191
  const workflow = this.workflows.get(workflowId);
725
1192
  if (!workflow)
726
1193
  return;
727
- await this.transitionToPhase(workflowId, 'implementing');
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');
728
1197
  try {
729
1198
  log.info('Implementing task', { taskId: workflow.taskId }, 'vibing-orchestrator');
730
1199
  // Start task execution with enhanced prompts and workflow context
@@ -737,6 +1206,14 @@ export class VibingOrchestrator extends EventEmitter {
737
1206
  (workflow.failureContext?.atPhase
738
1207
  ? `Retry after ${workflow.failureContext.atPhase} failure`
739
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;
740
1217
  // Prepare to capture executionId from event, then start execution
741
1218
  const createdPromise = this.waitForExecutionCreated(workflow.taskId, workflowId, 20000);
742
1219
  // Fire-and-forget; attach catch to prevent unhandled rejection from crashing process
@@ -748,6 +1225,7 @@ export class VibingOrchestrator extends EventEmitter {
748
1225
  previousExecutionId: previousExecId,
749
1226
  failurePhase: workflow.failureContext?.atPhase,
750
1227
  qualityResults: workflow.qualityResults,
1228
+ aiReview: aiReviewContext,
751
1229
  attempt,
752
1230
  }
753
1231
  : undefined,
@@ -774,17 +1252,16 @@ export class VibingOrchestrator extends EventEmitter {
774
1252
  await this.saveWorkflow(workflow);
775
1253
  }
776
1254
  log.info('Implementation completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
777
- // Update checkpoint status
778
- workflow.status = 'implemented';
779
- workflow.lastUpdatedAt = new Date().toISOString();
780
- await this.saveWorkflow(workflow);
781
- await this.transitionToPhase(workflowId, 'implemented');
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 });
782
1261
  if (workflow.metadata.stepByStepMode) {
783
1262
  log.info('Manual step mode – implemented; waiting for Continue to run validation', { workflowId }, 'vibing-orchestrator');
784
1263
  }
785
- else {
786
- await this.executeValidation(workflowId);
787
- }
1264
+ // Note: In non-stepByStepMode, executeValidation is triggered by reducer's EXECUTE_VALIDATION effect
788
1265
  }
789
1266
  catch (error) {
790
1267
  await this.handlePhaseFailure(workflowId, 'implementing', error);
@@ -792,14 +1269,30 @@ export class VibingOrchestrator extends EventEmitter {
792
1269
  }
793
1270
  /**
794
1271
  * Public: re-run implementation phase on demand
795
- * Forces phase to 'implementing' and executes implementation again.
1272
+ * Uses dispatch(RETRY) to transition FSM and trigger implementation via effect.
796
1273
  */
797
1274
  async rerunImplementation(workflowId, feedback, providerOverride) {
798
1275
  const workflow = this.workflows.get(workflowId);
799
1276
  if (!workflow)
800
1277
  throw new Error(`Workflow ${workflowId} not found`);
801
- await this.transitionToPhase(workflowId, 'implementing');
802
- await this.executeImplementation(workflowId, feedback, providerOverride);
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' });
803
1296
  }
804
1297
  /**
805
1298
  * Get previous phase, preferring the phase where failure occurred
@@ -827,8 +1320,8 @@ export class VibingOrchestrator extends EventEmitter {
827
1320
  await this.resetFromPhase(workflowId, phase);
828
1321
  switch (phase) {
829
1322
  case 'implementing':
830
- await this.transitionToPhase(workflowId, 'implementing');
831
- await this.executeImplementation(workflowId);
1323
+ // Use dispatch(RETRY) to transition FSM and trigger implementation via effect
1324
+ await this.dispatch(workflowId, { type: 'RETRY' });
832
1325
  return;
833
1326
  case 'validating':
834
1327
  await this.executeValidation(workflowId);
@@ -851,7 +1344,7 @@ export class VibingOrchestrator extends EventEmitter {
851
1344
  return;
852
1345
  default:
853
1346
  // For other phases, restart implementation as a safe default
854
- await this.executeImplementation(workflowId);
1347
+ await this.dispatch(workflowId, { type: 'RETRY' });
855
1348
  return;
856
1349
  }
857
1350
  }
@@ -945,6 +1438,7 @@ export class VibingOrchestrator extends EventEmitter {
945
1438
  ...workflow.metadata,
946
1439
  aiReviewResult: { ...review, timestamp },
947
1440
  };
1441
+ workflow.aiReviewResult = { ...review, timestamp };
948
1442
  // Track execution ID under ai-reviewing for observability
949
1443
  const executionId = providedExecutionId ?? review?.executionId;
950
1444
  if (executionId) {
@@ -979,12 +1473,15 @@ export class VibingOrchestrator extends EventEmitter {
979
1473
  }
980
1474
  /**
981
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.
982
1478
  */
983
1479
  async executeAiReviewPhase(workflowId) {
984
1480
  const workflow = this.workflows.get(workflowId);
985
1481
  if (!workflow)
986
1482
  return;
987
- await this.transitionToPhase(workflowId, 'ai-reviewing');
1483
+ // Update UI phase for timeline display (FSM stays in 'approved')
1484
+ this.updateUIPhase(workflow, 'ai-reviewing');
988
1485
  try {
989
1486
  const attemptList = workflow.executionIds?.['ai-reviewing'] || [];
990
1487
  const attempt = attemptList.length + 1;
@@ -1051,7 +1548,11 @@ export class VibingOrchestrator extends EventEmitter {
1051
1548
  const workflow = this.workflows.get(workflowId);
1052
1549
  if (!workflow)
1053
1550
  return;
1054
- await this.transitionToPhase(workflowId, 'awaiting-review');
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);
1055
1556
  const hasReal = Array.isArray(workflow.timeline)
1056
1557
  ? workflow.timeline.some((t) => t.phase === 'awaiting-review' && !t.placeholder)
1057
1558
  : false;
@@ -1063,17 +1564,18 @@ export class VibingOrchestrator extends EventEmitter {
1063
1564
  startTime: new Date().toISOString(),
1064
1565
  });
1065
1566
  }
1066
- workflow.timeline = this.computeVisibleTimeline(workflow);
1067
1567
  await this.saveWorkflow(workflow);
1068
1568
  }
1069
1569
  /**
1070
1570
  * Execute validation phase with quality checks
1571
+ * Note: FSM has already transitioned to 'validating' via IMPL_SUCCESS event
1071
1572
  */
1072
1573
  async executeValidation(workflowId) {
1073
1574
  const workflow = this.workflows.get(workflowId);
1074
1575
  if (!workflow)
1075
1576
  return;
1076
- await this.transitionToPhase(workflowId, 'validating');
1577
+ // Update UI phase for timeline display (FSM is already in 'validating')
1578
+ this.updateUIPhase(workflow, 'validating');
1077
1579
  try {
1078
1580
  if (!workflow.metadata.autoQualityChecks) {
1079
1581
  // TODO implement later
@@ -1088,6 +1590,17 @@ export class VibingOrchestrator extends EventEmitter {
1088
1590
  workflowId,
1089
1591
  workingDirectory,
1090
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
+ }
1091
1604
  workflow.executionIds = workflow.executionIds || {};
1092
1605
  const valList = workflow.executionIds['validating'] || [];
1093
1606
  workflow.executionIds['validating'] = [...valList, execId];
@@ -1156,19 +1669,15 @@ export class VibingOrchestrator extends EventEmitter {
1156
1669
  return;
1157
1670
  }
1158
1671
  log.info('Quality checks passed', { taskId: workflow.taskId }, 'vibing-orchestrator');
1159
- // Update checkpoint status
1160
- await this.transitionToPhase(workflowId, 'validated');
1161
- workflow.status = 'validated';
1162
- workflow.lastUpdatedAt = new Date().toISOString();
1163
- await this.saveWorkflow(workflow);
1164
- if (workflow.metadata.aiCodeReview) {
1165
- if (workflow.metadata.stepByStepMode) {
1166
- log.info('Manual step mode – waiting for Continue to run AI review', { workflowId }, 'vibing-orchestrator');
1167
- }
1168
- else {
1169
- await this.executeAiReviewPhase(workflowId);
1170
- }
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');
1171
1679
  }
1680
+ // Note: In non-stepByStepMode with aiCodeReview, executeAiReviewPhase is triggered by reducer's EXECUTE_AI_REVIEW effect
1172
1681
  }
1173
1682
  catch (error) {
1174
1683
  await this.handlePhaseFailure(workflowId, 'validating', error);
@@ -1185,12 +1694,14 @@ export class VibingOrchestrator extends EventEmitter {
1185
1694
  }
1186
1695
  /**
1187
1696
  * Execute merge phase
1697
+ * Note: FSM has already transitioned to 'merging' via APPROVE event
1188
1698
  */
1189
1699
  async executeMerge(workflowId, providerOverride) {
1190
1700
  const workflow = this.workflows.get(workflowId);
1191
1701
  if (!workflow)
1192
1702
  return;
1193
- await this.transitionToPhase(workflowId, 'merging');
1703
+ // Update UI phase for timeline display (FSM is already in 'merging')
1704
+ this.updateUIPhase(workflow, 'merging');
1194
1705
  let executionId;
1195
1706
  try {
1196
1707
  log.info('AI merging changes', { taskId: workflow.taskId }, 'vibing-orchestrator');
@@ -1227,30 +1738,39 @@ export class VibingOrchestrator extends EventEmitter {
1227
1738
  if (!mergeExec || mergeExec.status !== 'completed') {
1228
1739
  throw new Error(`AI merge failed: ${mergeExec?.error || 'Unknown error'}`);
1229
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
+ }
1230
1746
  const latest = this.workflows.get(workflowId) || workflow;
1231
1747
  const timeline = (latest.timeline || []);
1232
1748
  const tMerge = timeline.find((t) => t.executionId === executionId);
1233
1749
  if (tMerge) {
1234
1750
  tMerge.endTime = mergeExec.endTime || new Date().toISOString();
1235
1751
  tMerge.usage = mergeExec.usage;
1752
+ if (mergeVerification.mergeCommit) {
1753
+ tMerge.mergeCommit = mergeVerification.mergeCommit;
1754
+ }
1236
1755
  latest.timeline = this.computeVisibleTimeline(latest);
1237
1756
  latest.lastUpdatedAt = new Date().toISOString();
1238
1757
  await this.saveWorkflow(latest);
1239
1758
  }
1240
- await this.taskService.updateTask(workflow.taskId, { status: 'done' });
1241
- await this.transitionToPhase(workflowId, 'merged');
1242
- const mergedWorkflow = this.workflows.get(workflowId);
1243
- if (mergedWorkflow) {
1244
- mergedWorkflow.status = 'merged';
1245
- mergedWorkflow.lastUpdatedAt = new Date().toISOString();
1246
- await this.saveWorkflow(mergedWorkflow);
1759
+ // Store merge commit in workflow links
1760
+ if (mergeVerification.mergeCommit) {
1761
+ workflow.links = workflow.links || {};
1762
+ workflow.links.mergeCommit = mergeVerification.mergeCommit;
1247
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' });
1248
1770
  if (workflow.metadata.stepByStepMode) {
1249
1771
  log.info('Manual step mode – merge completed; waiting for Continue', { workflowId }, 'vibing-orchestrator');
1250
1772
  }
1251
- else {
1252
- await this.updateCleanupStatus(workflowId);
1253
- }
1773
+ // Note: In non-stepByStepMode, executeCleanup is triggered by reducer's EXECUTE_CLEANUP effect
1254
1774
  log.info('Merge completed, ready for cleanup', { taskId: workflow.taskId }, 'vibing-orchestrator');
1255
1775
  }
1256
1776
  catch (error) {
@@ -1271,21 +1791,98 @@ export class VibingOrchestrator extends EventEmitter {
1271
1791
  await this.handlePhaseFailure(workflowId, 'merging', error);
1272
1792
  }
1273
1793
  }
1274
- async updateCleanupStatus(workflowId) {
1275
- const workflow = this.workflows.get(workflowId);
1276
- if (!workflow)
1277
- return;
1278
- await this.transitionToPhase(workflowId, 'cleaning');
1279
- workflow.status = 'cleaning';
1280
- workflow.lastUpdatedAt = new Date().toISOString();
1281
- workflow.timeline = this.buildTimeline(workflow, {
1282
- id: `cleaning:${new Date().toISOString()}`,
1283
- label: 'Cleaning',
1284
- phase: 'cleaning',
1285
- startTime: new Date().toISOString(),
1286
- });
1287
- await this.saveWorkflow(workflow);
1288
- this.emit('workflowUpdated', workflow);
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
+ }
1289
1886
  }
1290
1887
  /**
1291
1888
  * Public: run merge phase on demand
@@ -1299,12 +1896,15 @@ export class VibingOrchestrator extends EventEmitter {
1299
1896
  }
1300
1897
  /**
1301
1898
  * Execute cleanup phase: remove worktree/branch and finalize workflow
1899
+ * Note: FSM has already transitioned to 'done' via MERGE_SUCCESS event
1302
1900
  */
1303
1901
  async executeCleanup(workflowId) {
1304
1902
  const workflow = this.workflows.get(workflowId);
1305
1903
  if (!workflow)
1306
1904
  return;
1307
- await this.transitionToPhase(workflowId, 'cleaning');
1905
+ // Update UI phase for timeline display
1906
+ workflow.status = 'cleaning';
1907
+ this.updateUIPhase(workflow, 'cleaning');
1308
1908
  try {
1309
1909
  log.info('Cleaning up resources', { taskId: workflow.taskId }, 'vibing-orchestrator');
1310
1910
  // Remove worktree and local branch if present
@@ -1314,12 +1914,14 @@ export class VibingOrchestrator extends EventEmitter {
1314
1914
  catch (err) {
1315
1915
  log.warn('Cleanup encountered an issue (continuing)', err, 'vibing-orchestrator');
1316
1916
  }
1317
- // Finalize workflow
1917
+ // Finalize workflow - update UI phases
1318
1918
  workflow.status = 'cleaned';
1319
1919
  workflow.endTime = new Date().toISOString();
1320
- await this.transitionToPhase(workflowId, 'cleaned');
1920
+ this.updateUIPhase(workflow, 'cleaned');
1921
+ // Dispatch FSM event for cleanup completion
1922
+ await this.dispatch(workflowId, { type: 'CLEAN_SUCCESS' });
1321
1923
  workflow.status = 'completed';
1322
- workflow.lastUpdatedAt = new Date().toISOString();
1924
+ this.updateUIPhase(workflow, 'completed');
1323
1925
  await this.saveWorkflow(workflow);
1324
1926
  this.emit('workflowCompleted', workflow);
1325
1927
  log.info('Workflow completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
@@ -1332,9 +1934,13 @@ export class VibingOrchestrator extends EventEmitter {
1332
1934
  error: workflow.error,
1333
1935
  timestamp: new Date().toISOString(),
1334
1936
  };
1335
- await this.transitionToPhase(workflowId, 'failed');
1937
+ // Dispatch failure event
1938
+ await this.dispatch(workflowId, {
1939
+ type: 'CLEAN_FAIL',
1940
+ error: workflow.error,
1941
+ });
1336
1942
  workflow.status = 'failed';
1337
- workflow.lastUpdatedAt = new Date().toISOString();
1943
+ this.updateUIPhase(workflow, 'failed');
1338
1944
  await this.saveWorkflow(workflow);
1339
1945
  this.emit('workflowFailed', workflow);
1340
1946
  }
@@ -1396,11 +2002,13 @@ export class VibingOrchestrator extends EventEmitter {
1396
2002
  const nextPhase = idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : 'completed';
1397
2003
  if (nextPhase && nextPhase !== workflow.phase) {
1398
2004
  log.info('Manually skipping workflow phase', { workflowId, currentPhase: workflow.phase, basePhase, nextPhase }, 'vibing-orchestrator');
1399
- await this.transitionToPhase(workflowId, nextPhase);
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);
1400
2008
  // Immediately kick off the next phase where applicable to make skipping faster
1401
2009
  switch (nextPhase) {
1402
2010
  case 'implementing':
1403
- await this.executeImplementation(workflowId);
2011
+ await this.dispatch(workflowId, { type: 'RETRY' });
1404
2012
  break;
1405
2013
  case 'validating':
1406
2014
  await this.executeValidation(workflowId);
@@ -1612,32 +2220,32 @@ export class VibingOrchestrator extends EventEmitter {
1612
2220
  }
1613
2221
  /**
1614
2222
  * Continue workflow execution from current state
2223
+ * Uses the new state machine to determine the next action
1615
2224
  */
1616
2225
  async continueWorkflow(workflowId) {
1617
2226
  const workflow = this.workflows.get(workflowId);
1618
2227
  if (!workflow) {
1619
2228
  throw new Error(`Workflow ${workflowId} not found`);
1620
2229
  }
1621
- log.info('Continuing workflow (status-driven)', { workflowId, phase: workflow.phase, status: workflow.status }, 'vibing-orchestrator');
1622
- // If failed, prefer immediate retry of implementation
1623
- if (workflow.phase === 'failed' || workflow.status === 'failed') {
1624
- if (workflow.metadata.stepByStepMode) {
1625
- await this.rerunImplementation(workflowId);
1626
- }
1627
- else {
1628
- await this.executeImplementation(workflowId);
1629
- }
1630
- return;
1631
- }
1632
- // Compute next actionable phase from status/checkpoint
1633
- const next = this.computeNextActionPhase(workflow);
1634
- if (!next) {
1635
- log.info('No actionable next phase (waiting for human or already done)', { workflowId, phase: workflow.phase, status: workflow.status }, 'vibing-orchestrator');
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');
1636
2244
  return;
1637
2245
  }
1638
- switch (next) {
2246
+ switch (nextAction.action) {
1639
2247
  case 'implementing':
1640
- await this.executeImplementation(workflowId);
2248
+ await this.executeImplementation(workflowId, nextAction.feedback);
1641
2249
  return;
1642
2250
  case 'validating':
1643
2251
  await this.executeValidation(workflowId);
@@ -1645,56 +2253,17 @@ export class VibingOrchestrator extends EventEmitter {
1645
2253
  case 'ai-reviewing':
1646
2254
  await this.executeAiReviewPhase(workflowId);
1647
2255
  return;
1648
- case 'awaiting-review':
2256
+ case 'awaiting-approval':
1649
2257
  await this.executeAwaitingReview(workflowId);
1650
2258
  return;
1651
2259
  case 'merging':
1652
2260
  await this.executeMerge(workflowId);
1653
2261
  return;
1654
- case 'cleaning':
2262
+ case 'cleanup':
1655
2263
  await this.executeCleanup(workflowId);
1656
2264
  return;
1657
2265
  default:
1658
- throw new Error(`Unsupported next phase computed: ${next}`);
1659
- }
1660
- }
1661
- // Decide next action from status/checkpoint and configuration
1662
- computeNextActionPhase(workflow) {
1663
- const status = workflow.status;
1664
- const cfg = workflow.metadata || {};
1665
- const requireHuman = !!cfg.requireHumanApproval;
1666
- const wantsAIReview = !!cfg.aiCodeReview;
1667
- // If paused, compute by last checkpoint
1668
- // If failed handling is done earlier
1669
- switch (status) {
1670
- case undefined:
1671
- case 'draft':
1672
- return 'implementing';
1673
- case 'implemented':
1674
- return 'validating';
1675
- case 'validated':
1676
- if (wantsAIReview)
1677
- return 'ai-reviewing';
1678
- return requireHuman ? null : 'merging';
1679
- case 'ai-reviewed':
1680
- return requireHuman ? null : 'merging';
1681
- case 'awaiting-review':
1682
- return null; // wait for approveWorkflow
1683
- case 'approved':
1684
- return 'merging';
1685
- case 'merged':
1686
- return 'cleaning';
1687
- case 'cleaned':
1688
- return 'completed';
1689
- case 'completed':
1690
- return null;
1691
- case 'paused':
1692
- // Map paused to next by lastPhase or default to implementing
1693
- return workflow.lastPhase || 'implementing';
1694
- case 'failed':
1695
- return 'implementing';
1696
- default:
1697
- return null;
2266
+ log.warn('Unknown next action from state machine', { workflowId, action: nextAction.action }, 'vibing-orchestrator');
1698
2267
  }
1699
2268
  }
1700
2269
  // ============ Small helpers to reduce duplication ============
@@ -1778,8 +2347,14 @@ export class VibingOrchestrator extends EventEmitter {
1778
2347
  const max = workflow.metadata.retryPolicy?.maxImplementationAttempts || 0;
1779
2348
  const implementingCount = (workflow.phaseHistory || []).filter((h) => h.to === 'implementing').length;
1780
2349
  if (max > 0 && implementingCount < max) {
1781
- await this.transitionToPhase(workflowId, 'implementing');
1782
- await this.executeImplementation(workflowId, reason);
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' });
1783
2358
  return true;
1784
2359
  }
1785
2360
  return false;
@@ -1806,12 +2381,31 @@ export class VibingOrchestrator extends EventEmitter {
1806
2381
  }
1807
2382
  await this.saveWorkflow(wf);
1808
2383
  }
1809
- await this.transitionToPhase(workflowId, 'failed');
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
1810
2390
  workflow.status = 'failed';
1811
- workflow.lastUpdatedAt = new Date().toISOString();
2391
+ this.updateUIPhase(workflow, 'failed');
1812
2392
  await this.saveWorkflow(workflow);
1813
2393
  this.emit('workflowFailed', workflow);
1814
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
+ }
1815
2409
  /**
1816
2410
  * Get the agent service instance
1817
2411
  */
@@ -1831,6 +2425,14 @@ function serializeVibe(workflow) {
1831
2425
  timestamp: new Date(workflow.qualityResults.timestamp).toISOString(),
1832
2426
  }
1833
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,
1834
2436
  };
1835
2437
  }
1836
2438
  async function getDbStats(db) {