vibeman 0.0.3 → 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 (184) 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 +4 -0
  4. package/dist/runtime/api/agent/agent-service.js +62 -3
  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.js +40 -12
  8. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.d.ts +24 -0
  9. package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.js +291 -0
  10. package/dist/runtime/api/agent/ai-providers/index.d.ts +3 -3
  11. package/dist/runtime/api/agent/ai-providers/index.js +3 -1
  12. package/dist/runtime/api/agent/ai-providers/types.d.ts +5 -2
  13. package/dist/runtime/api/agent/amp-cli-provider.test.d.ts +1 -0
  14. package/dist/runtime/api/agent/amp-cli-provider.test.js +99 -0
  15. package/dist/runtime/api/agent/codex-cli-provider.test.js +10 -8
  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 +2 -2
  19. package/dist/runtime/api/agent/routing-policy.test.js +4 -4
  20. package/dist/runtime/api/api/routers/ai.d.ts +3 -3
  21. package/dist/runtime/api/api/routers/executions.d.ts +2 -7
  22. package/dist/runtime/api/api/routers/executions.js +2 -2
  23. package/dist/runtime/api/api/routers/provider-config.d.ts +34 -0
  24. package/dist/runtime/api/api/routers/settings.d.ts +19 -0
  25. package/dist/runtime/api/api/routers/settings.js +16 -0
  26. package/dist/runtime/api/api/routers/tasks.d.ts +9 -9
  27. package/dist/runtime/api/api/routers/workflows.d.ts +12 -12
  28. package/dist/runtime/api/api/routers/worktrees.d.ts +2 -2
  29. package/dist/runtime/api/api/trpc.d.ts +16 -16
  30. package/dist/runtime/api/lib/local-config.d.ts +94 -4
  31. package/dist/runtime/api/lib/local-config.js +16 -0
  32. package/dist/runtime/api/lib/provider-detection.d.ts +2 -0
  33. package/dist/runtime/api/lib/provider-detection.js +83 -1
  34. package/dist/runtime/api/lib/server/vibeman-info.d.ts +5 -0
  35. package/dist/runtime/api/lib/server/vibeman-info.js +85 -0
  36. package/dist/runtime/api/lib/trpc/server.d.ts +63 -33
  37. package/dist/runtime/api/persistence/execution-log-persistence.d.ts +1 -1
  38. package/dist/runtime/api/persistence/execution-log-persistence.js +19 -3
  39. package/dist/runtime/api/router.d.ts +63 -33
  40. package/dist/runtime/api/settings-service.js +31 -14
  41. package/dist/runtime/api/tasks/task-file-parser.d.ts +1 -0
  42. package/dist/runtime/api/tasks/task-file-parser.js +20 -1
  43. package/dist/runtime/api/tasks/task-updater.d.ts +62 -0
  44. package/dist/runtime/api/tasks/task-updater.js +260 -0
  45. package/dist/runtime/api/tasks/task-updater.test.d.ts +1 -0
  46. package/dist/runtime/api/tasks/task-updater.test.js +303 -0
  47. package/dist/runtime/api/types/index.d.ts +1 -1
  48. package/dist/runtime/api/types/settings.d.ts +17 -6
  49. package/dist/runtime/api/vcs/git-service.d.ts +9 -0
  50. package/dist/runtime/api/vcs/git-service.js +23 -0
  51. package/dist/runtime/api/vcs/worktree-service.d.ts +1 -1
  52. package/dist/runtime/api/vcs/worktree-service.js +22 -10
  53. package/dist/runtime/api/workflows/quality-pipeline.js +2 -1
  54. package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +93 -5
  55. package/dist/runtime/api/workflows/vibing-orchestrator.js +774 -203
  56. package/dist/runtime/api/workflows/workflow-effects.d.ts +45 -0
  57. package/dist/runtime/api/workflows/workflow-effects.js +49 -0
  58. package/dist/runtime/api/workflows/workflow-reconciler.d.ts +65 -0
  59. package/dist/runtime/api/workflows/workflow-reconciler.js +226 -0
  60. package/dist/runtime/api/workflows/workflow-reducer.d.ts +26 -0
  61. package/dist/runtime/api/workflows/workflow-reducer.js +288 -0
  62. package/dist/runtime/api/workflows/workflow-reducer.test.d.ts +1 -0
  63. package/dist/runtime/api/workflows/workflow-reducer.test.js +247 -0
  64. package/dist/runtime/api/workflows/workflow-schema.d.ts +546 -0
  65. package/dist/runtime/api/workflows/workflow-schema.js +256 -0
  66. package/dist/runtime/web/.next/BUILD_ID +1 -1
  67. package/dist/runtime/web/.next/app-build-manifest.json +50 -50
  68. package/dist/runtime/web/.next/app-path-routes-manifest.json +1 -1
  69. package/dist/runtime/web/.next/build-manifest.json +14 -14
  70. package/dist/runtime/web/.next/prerender-manifest.json +3 -3
  71. package/dist/runtime/web/.next/react-loadable-manifest.json +2 -33
  72. package/dist/runtime/web/.next/required-server-files.json +5 -5
  73. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +1 -1
  74. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -1
  75. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -1
  76. package/dist/runtime/web/.next/server/app/_not-found/page.js +2 -2
  77. package/dist/runtime/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  78. package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  79. package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
  80. package/dist/runtime/web/.next/server/app/_not-found.rsc +12 -12
  81. package/dist/runtime/web/.next/server/app/api/health/route.js +1 -1
  82. package/dist/runtime/web/.next/server/app/api/health/route.js.nft.json +1 -1
  83. package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  84. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +1 -1
  85. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js.nft.json +1 -1
  86. package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
  87. package/dist/runtime/web/.next/server/app/api/upload/route.js +1 -1
  88. package/dist/runtime/web/.next/server/app/api/upload/route.js.nft.json +1 -1
  89. package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
  90. package/dist/runtime/web/.next/server/app/index.html +2 -2
  91. package/dist/runtime/web/.next/server/app/index.rsc +15 -15
  92. package/dist/runtime/web/.next/server/app/page.js +27 -62
  93. package/dist/runtime/web/.next/server/app/page.js.nft.json +1 -1
  94. package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/dist/runtime/web/.next/server/app-paths-manifest.json +1 -1
  96. package/dist/runtime/web/.next/server/chunks/210.js +1 -0
  97. package/dist/runtime/web/.next/server/chunks/291.js +18 -0
  98. package/dist/runtime/web/.next/server/chunks/552.js +22 -0
  99. package/dist/runtime/web/.next/server/chunks/780.js +1 -0
  100. package/dist/runtime/web/.next/server/chunks/905.js +6 -0
  101. package/dist/runtime/web/.next/server/chunks/98.js +1 -0
  102. package/dist/runtime/web/.next/server/middleware-build-manifest.js +1 -1
  103. package/dist/runtime/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  104. package/dist/runtime/web/.next/server/pages/404.html +2 -2
  105. package/dist/runtime/web/.next/server/pages/500.html +1 -1
  106. package/dist/runtime/web/.next/server/pages/_app.js +1 -1
  107. package/dist/runtime/web/.next/server/pages/_app.js.nft.json +1 -1
  108. package/dist/runtime/web/.next/server/pages/_document.js +1 -1
  109. package/dist/runtime/web/.next/server/pages/_document.js.nft.json +1 -1
  110. package/dist/runtime/web/.next/server/pages/_error.js +9 -9
  111. package/dist/runtime/web/.next/server/pages/_error.js.nft.json +1 -1
  112. package/dist/runtime/web/.next/server/pages-manifest.json +1 -1
  113. package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
  114. package/dist/runtime/web/.next/server/webpack-runtime.js +1 -1
  115. package/dist/runtime/web/.next/static/{5_15u1WQCxN1_eHZpldCv → LJFZk_8tvKFN_Ee4HqUuM}/_buildManifest.js +1 -1
  116. package/dist/runtime/web/.next/static/chunks/05c91ade-7d09b2b280adffd1.js +1 -0
  117. package/dist/runtime/web/.next/static/chunks/201-51bef3fa8c832e2e.js +1 -0
  118. package/dist/runtime/web/.next/static/chunks/524-89747ed9b0294f8a.js +1 -0
  119. package/dist/runtime/web/.next/static/chunks/554-8bec6e9cca6acc67.js +1 -0
  120. package/dist/runtime/web/.next/static/chunks/764.86e9503a69d45a85.js +1 -0
  121. package/dist/runtime/web/.next/static/chunks/{87c73c54-09e1ba5c70e60a51.js → 7ab4dc20-239138e0ae7af24a.js} +1 -1
  122. package/dist/runtime/web/.next/static/chunks/905-342391e3d3a3678f.js +20 -0
  123. package/dist/runtime/web/.next/static/chunks/a8a5ce16-4edea7df2d9b544a.js +79 -0
  124. package/dist/runtime/web/.next/static/chunks/{8bb4d8db-3e2aa02b0a2384b9.js → ad74d572-4c1b162e2c15acaa.js} +1 -1
  125. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-7b752a8641f96c1f.js +1 -0
  126. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-34e66b251c2b5044.js +1 -0
  127. package/dist/runtime/web/.next/static/chunks/app/api/health/route-7b752a8641f96c1f.js +1 -0
  128. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-7b752a8641f96c1f.js +1 -0
  129. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-7b752a8641f96c1f.js +1 -0
  130. package/dist/runtime/web/.next/static/chunks/app/layout-df9ac93cb02b2385.js +1 -0
  131. package/dist/runtime/web/.next/static/chunks/app/page-6610743f7de5f92a.js +1 -0
  132. package/dist/runtime/web/.next/static/chunks/c25e0690-e9b798b8de667da1.js +1 -0
  133. package/dist/runtime/web/.next/static/chunks/framework-57157ec4d37f64aa.js +1 -0
  134. package/dist/runtime/web/.next/static/chunks/main-app-156cc0c60371bd78.js +1 -0
  135. package/dist/runtime/web/.next/static/chunks/main-df25d367c47b1fec.js +1 -0
  136. package/dist/runtime/web/.next/static/chunks/pages/_app-9f629a5e1131d19f.js +1 -0
  137. package/dist/runtime/web/.next/static/chunks/pages/_error-9238238274c7efcd.js +1 -0
  138. package/dist/runtime/web/.next/static/chunks/webpack-cd50e39b423d1808.js +1 -0
  139. package/dist/runtime/web/.next/static/css/4fbf378a264bd4ea.css +1 -0
  140. package/dist/runtime/web/package.json +8 -8
  141. package/dist/runtime/web/server.js +1 -1
  142. package/dist/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +3 -37
  144. package/dist/runtime/api/lib/trpc/client.d.ts +0 -1
  145. package/dist/runtime/api/lib/trpc/client.js +0 -5
  146. package/dist/runtime/web/.next/server/chunks/217.js +0 -1
  147. package/dist/runtime/web/.next/server/chunks/383.js +0 -6
  148. package/dist/runtime/web/.next/server/chunks/458.js +0 -1
  149. package/dist/runtime/web/.next/server/chunks/576.js +0 -18
  150. package/dist/runtime/web/.next/server/chunks/635.js +0 -22
  151. package/dist/runtime/web/.next/server/chunks/761.js +0 -1
  152. package/dist/runtime/web/.next/server/chunks/777.js +0 -3
  153. package/dist/runtime/web/.next/server/chunks/825.js +0 -1
  154. package/dist/runtime/web/.next/server/chunks/838.js +0 -1
  155. package/dist/runtime/web/.next/server/chunks/973.js +0 -15
  156. package/dist/runtime/web/.next/static/chunks/18-15c10d3288afef2e.js +0 -1
  157. package/dist/runtime/web/.next/static/chunks/1c0ca389.537bbe362e3ffbd9.js +0 -3
  158. package/dist/runtime/web/.next/static/chunks/22747d63-ad5da0c19f4cfe41.js +0 -71
  159. package/dist/runtime/web/.next/static/chunks/355.056c2645878a799a.js +0 -1
  160. package/dist/runtime/web/.next/static/chunks/420.a5ccf151c9e2b2f1.js +0 -1
  161. package/dist/runtime/web/.next/static/chunks/439.1be0c6242fd248d5.js +0 -15
  162. package/dist/runtime/web/.next/static/chunks/440.c52e7c0f797e22b2.js +0 -1
  163. package/dist/runtime/web/.next/static/chunks/575-e2478287c27da87b.js +0 -1
  164. package/dist/runtime/web/.next/static/chunks/691.920d88c115087314.js +0 -1
  165. package/dist/runtime/web/.next/static/chunks/765-e838910065b50c3d.js +0 -1
  166. package/dist/runtime/web/.next/static/chunks/823-6f371a6e829adbba.js +0 -63
  167. package/dist/runtime/web/.next/static/chunks/891cff7f.0f71fc028f87e683.js +0 -1
  168. package/dist/runtime/web/.next/static/chunks/9af238c7-271a911d4e99ab18.js +0 -1
  169. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-751c9265a65409e5.js +0 -1
  170. package/dist/runtime/web/.next/static/chunks/app/_not-found/page-1cb74d1cba27d0ab.js +0 -1
  171. package/dist/runtime/web/.next/static/chunks/app/api/health/route-751c9265a65409e5.js +0 -1
  172. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-751c9265a65409e5.js +0 -1
  173. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-751c9265a65409e5.js +0 -1
  174. package/dist/runtime/web/.next/static/chunks/app/layout-8435322f09fd0975.js +0 -1
  175. package/dist/runtime/web/.next/static/chunks/app/page-9fe7d75095b4ccec.js +0 -1
  176. package/dist/runtime/web/.next/static/chunks/cac567b0-5b77dd12911823cd.js +0 -1
  177. package/dist/runtime/web/.next/static/chunks/framework-2518f1345b5b2806.js +0 -1
  178. package/dist/runtime/web/.next/static/chunks/main-17665e5e39de9a8a.js +0 -1
  179. package/dist/runtime/web/.next/static/chunks/main-app-c0b0f5ba4f7f9d75.js +0 -1
  180. package/dist/runtime/web/.next/static/chunks/pages/_app-d6f6b3bbc3d81ee1.js +0 -1
  181. package/dist/runtime/web/.next/static/chunks/pages/_error-75a96cf1997cc3b9.js +0 -1
  182. package/dist/runtime/web/.next/static/chunks/webpack-c8de37305b4635cf.js +0 -1
  183. package/dist/runtime/web/.next/static/css/08c950681f1a9a92.css +0 -1
  184. /package/dist/runtime/web/.next/static/{5_15u1WQCxN1_eHZpldCv → 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,
@@ -476,11 +898,9 @@ export class VibingOrchestrator extends EventEmitter {
476
898
  ...workflow.metadata,
477
899
  humanCompleted: true,
478
900
  };
479
- workflow.endTime = new Date().toISOString();
480
- await this.transitionToPhase(workflowId, 'completed');
481
- this.emit('workflowCompleted', workflow);
901
+ // Use dispatch for manual completion
902
+ await this.dispatch(workflowId, { type: 'COMPLETE_MANUAL' });
482
903
  log.info('Workflow marked completed by human', { taskId: workflow.taskId }, 'vibing-orchestrator');
483
- await this.saveWorkflow(workflow);
484
904
  }
485
905
  async ensureInitialized() {
486
906
  if (!this.initialized) {
@@ -532,12 +952,8 @@ export class VibingOrchestrator extends EventEmitter {
532
952
  log.warn('Failed to stop execution', error, 'vibing-orchestrator');
533
953
  }
534
954
  }
535
- workflow.phase = 'paused';
536
- workflow.error = 'Paused by user';
537
- workflow.status = 'paused';
538
- // Persist the workflow state
539
- await this.saveWorkflow(workflow);
540
- this.emit('workflowPaused', workflow);
955
+ // Use dispatch for state transition
956
+ await this.dispatch(workflowId, { type: 'PAUSE' });
541
957
  log.info('Paused workflow', { workflowId }, 'vibing-orchestrator');
542
958
  }
543
959
  /**
@@ -584,31 +1000,10 @@ export class VibingOrchestrator extends EventEmitter {
584
1000
  const workflow = this.requireWorkflow(workflowId);
585
1001
  this.assertPhase(workflow, 'awaiting-review', 'approve');
586
1002
  // Close the awaiting-review timeline item if present
587
- try {
588
- const tl = (workflow.timeline || []);
589
- for (let i = tl.length - 1; i >= 0; i--) {
590
- const t = tl[i];
591
- if (t.phase === 'awaiting-review' && !t.placeholder && !t.endTime) {
592
- t.endTime = new Date().toISOString();
593
- break;
594
- }
595
- }
596
- workflow.timeline = this.computeVisibleTimeline(workflow);
597
- await this.saveWorkflow(workflow);
598
- }
599
- catch (err) {
600
- log.debug('Failed to finalize awaiting-review timeline item', err, 'vibing-orchestrator');
601
- }
602
- workflow.status = 'approved';
603
- workflow.lastUpdatedAt = new Date().toISOString();
604
- await this.transitionToPhase(workflowId, 'approved');
605
- await this.saveWorkflow(workflow);
606
- if (workflow.metadata.stepByStepMode) {
607
- log.info('Manual step mode – approved; waiting for Continue to run merge', { workflowId }, 'vibing-orchestrator');
608
- }
609
- else {
610
- await this.executeMerge(workflowId);
611
- }
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');
612
1007
  }
613
1008
  /**
614
1009
  * Reject workflow and request changes
@@ -617,24 +1012,29 @@ export class VibingOrchestrator extends EventEmitter {
617
1012
  const workflow = this.requireWorkflow(workflowId);
618
1013
  this.assertPhase(workflow, 'awaiting-review', 'reject');
619
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) {
620
1024
  try {
621
1025
  const tl = (workflow.timeline || []);
622
1026
  for (let i = tl.length - 1; i >= 0; i--) {
623
1027
  const t = tl[i];
624
- if (t.phase === 'awaiting-review' && !t.placeholder && !t.endTime) {
1028
+ if (t.phase === phase && !t.placeholder && !t.endTime) {
625
1029
  t.endTime = new Date().toISOString();
626
1030
  break;
627
1031
  }
628
1032
  }
629
1033
  workflow.timeline = this.computeVisibleTimeline(workflow);
630
- await this.saveWorkflow(workflow);
631
1034
  }
632
1035
  catch (err) {
633
- log.debug('Failed to finalize awaiting-review timeline item', err, 'vibing-orchestrator');
1036
+ log.debug(`Failed to finalize ${phase} timeline item`, err, 'vibing-orchestrator');
634
1037
  }
635
- // Return to implementation phase with feedback
636
- await this.transitionToPhase(workflowId, 'implementing');
637
- await this.executeImplementation(workflowId, feedback);
638
1038
  }
639
1039
  /**
640
1040
  * Get workflow status
@@ -676,17 +1076,17 @@ export class VibingOrchestrator extends EventEmitter {
676
1076
  return latest;
677
1077
  }
678
1078
  /**
679
- * 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.
680
1081
  */
681
- async transitionToPhase(workflowId, newPhase) {
682
- const workflow = this.workflows.get(workflowId);
683
- if (!workflow)
684
- return;
1082
+ updateUIPhase(workflow, newPhase) {
685
1083
  const oldPhase = workflow.phase;
1084
+ if (oldPhase === newPhase)
1085
+ return;
686
1086
  workflow.lastPhase = oldPhase;
687
1087
  workflow.phase = newPhase;
688
- const history = workflow.phaseHistory || [];
689
1088
  const now = new Date().toISOString();
1089
+ const history = workflow.phaseHistory || [];
690
1090
  history.push({ from: oldPhase, to: newPhase, at: now });
691
1091
  workflow.phaseHistory = history;
692
1092
  // Metrics accumulation
@@ -704,9 +1104,74 @@ export class VibingOrchestrator extends EventEmitter {
704
1104
  }
705
1105
  workflow.lastUpdatedAt = now;
706
1106
  workflow.timeline = this.computeVisibleTimeline(workflow);
707
- log.info('Workflow phase transition', { workflowId, oldPhase, newPhase }, 'vibing-orchestrator');
708
- await this.saveWorkflow(workflow);
709
- 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;
710
1175
  }
711
1176
  /**
712
1177
  * Save workflow to persistence layer
@@ -726,7 +1191,9 @@ export class VibingOrchestrator extends EventEmitter {
726
1191
  const workflow = this.workflows.get(workflowId);
727
1192
  if (!workflow)
728
1193
  return;
729
- 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');
730
1197
  try {
731
1198
  log.info('Implementing task', { taskId: workflow.taskId }, 'vibing-orchestrator');
732
1199
  // Start task execution with enhanced prompts and workflow context
@@ -785,17 +1252,16 @@ export class VibingOrchestrator extends EventEmitter {
785
1252
  await this.saveWorkflow(workflow);
786
1253
  }
787
1254
  log.info('Implementation completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
788
- // Update checkpoint status
789
- workflow.status = 'implemented';
790
- workflow.lastUpdatedAt = new Date().toISOString();
791
- await this.saveWorkflow(workflow);
792
- 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 });
793
1261
  if (workflow.metadata.stepByStepMode) {
794
1262
  log.info('Manual step mode – implemented; waiting for Continue to run validation', { workflowId }, 'vibing-orchestrator');
795
1263
  }
796
- else {
797
- await this.executeValidation(workflowId);
798
- }
1264
+ // Note: In non-stepByStepMode, executeValidation is triggered by reducer's EXECUTE_VALIDATION effect
799
1265
  }
800
1266
  catch (error) {
801
1267
  await this.handlePhaseFailure(workflowId, 'implementing', error);
@@ -803,14 +1269,30 @@ export class VibingOrchestrator extends EventEmitter {
803
1269
  }
804
1270
  /**
805
1271
  * Public: re-run implementation phase on demand
806
- * Forces phase to 'implementing' and executes implementation again.
1272
+ * Uses dispatch(RETRY) to transition FSM and trigger implementation via effect.
807
1273
  */
808
1274
  async rerunImplementation(workflowId, feedback, providerOverride) {
809
1275
  const workflow = this.workflows.get(workflowId);
810
1276
  if (!workflow)
811
1277
  throw new Error(`Workflow ${workflowId} not found`);
812
- await this.transitionToPhase(workflowId, 'implementing');
813
- 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' });
814
1296
  }
815
1297
  /**
816
1298
  * Get previous phase, preferring the phase where failure occurred
@@ -838,8 +1320,8 @@ export class VibingOrchestrator extends EventEmitter {
838
1320
  await this.resetFromPhase(workflowId, phase);
839
1321
  switch (phase) {
840
1322
  case 'implementing':
841
- await this.transitionToPhase(workflowId, 'implementing');
842
- await this.executeImplementation(workflowId);
1323
+ // Use dispatch(RETRY) to transition FSM and trigger implementation via effect
1324
+ await this.dispatch(workflowId, { type: 'RETRY' });
843
1325
  return;
844
1326
  case 'validating':
845
1327
  await this.executeValidation(workflowId);
@@ -862,7 +1344,7 @@ export class VibingOrchestrator extends EventEmitter {
862
1344
  return;
863
1345
  default:
864
1346
  // For other phases, restart implementation as a safe default
865
- await this.executeImplementation(workflowId);
1347
+ await this.dispatch(workflowId, { type: 'RETRY' });
866
1348
  return;
867
1349
  }
868
1350
  }
@@ -991,12 +1473,15 @@ export class VibingOrchestrator extends EventEmitter {
991
1473
  }
992
1474
  /**
993
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.
994
1478
  */
995
1479
  async executeAiReviewPhase(workflowId) {
996
1480
  const workflow = this.workflows.get(workflowId);
997
1481
  if (!workflow)
998
1482
  return;
999
- await this.transitionToPhase(workflowId, 'ai-reviewing');
1483
+ // Update UI phase for timeline display (FSM stays in 'approved')
1484
+ this.updateUIPhase(workflow, 'ai-reviewing');
1000
1485
  try {
1001
1486
  const attemptList = workflow.executionIds?.['ai-reviewing'] || [];
1002
1487
  const attempt = attemptList.length + 1;
@@ -1063,7 +1548,11 @@ export class VibingOrchestrator extends EventEmitter {
1063
1548
  const workflow = this.workflows.get(workflowId);
1064
1549
  if (!workflow)
1065
1550
  return;
1066
- 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);
1067
1556
  const hasReal = Array.isArray(workflow.timeline)
1068
1557
  ? workflow.timeline.some((t) => t.phase === 'awaiting-review' && !t.placeholder)
1069
1558
  : false;
@@ -1075,17 +1564,18 @@ export class VibingOrchestrator extends EventEmitter {
1075
1564
  startTime: new Date().toISOString(),
1076
1565
  });
1077
1566
  }
1078
- workflow.timeline = this.computeVisibleTimeline(workflow);
1079
1567
  await this.saveWorkflow(workflow);
1080
1568
  }
1081
1569
  /**
1082
1570
  * Execute validation phase with quality checks
1571
+ * Note: FSM has already transitioned to 'validating' via IMPL_SUCCESS event
1083
1572
  */
1084
1573
  async executeValidation(workflowId) {
1085
1574
  const workflow = this.workflows.get(workflowId);
1086
1575
  if (!workflow)
1087
1576
  return;
1088
- await this.transitionToPhase(workflowId, 'validating');
1577
+ // Update UI phase for timeline display (FSM is already in 'validating')
1578
+ this.updateUIPhase(workflow, 'validating');
1089
1579
  try {
1090
1580
  if (!workflow.metadata.autoQualityChecks) {
1091
1581
  // TODO implement later
@@ -1179,19 +1669,15 @@ export class VibingOrchestrator extends EventEmitter {
1179
1669
  return;
1180
1670
  }
1181
1671
  log.info('Quality checks passed', { taskId: workflow.taskId }, 'vibing-orchestrator');
1182
- // Update checkpoint status
1183
- await this.transitionToPhase(workflowId, 'validated');
1184
- workflow.status = 'validated';
1185
- workflow.lastUpdatedAt = new Date().toISOString();
1186
- await this.saveWorkflow(workflow);
1187
- if (workflow.metadata.aiCodeReview) {
1188
- if (workflow.metadata.stepByStepMode) {
1189
- log.info('Manual step mode – waiting for Continue to run AI review', { workflowId }, 'vibing-orchestrator');
1190
- }
1191
- else {
1192
- await this.executeAiReviewPhase(workflowId);
1193
- }
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');
1194
1679
  }
1680
+ // Note: In non-stepByStepMode with aiCodeReview, executeAiReviewPhase is triggered by reducer's EXECUTE_AI_REVIEW effect
1195
1681
  }
1196
1682
  catch (error) {
1197
1683
  await this.handlePhaseFailure(workflowId, 'validating', error);
@@ -1208,12 +1694,14 @@ export class VibingOrchestrator extends EventEmitter {
1208
1694
  }
1209
1695
  /**
1210
1696
  * Execute merge phase
1697
+ * Note: FSM has already transitioned to 'merging' via APPROVE event
1211
1698
  */
1212
1699
  async executeMerge(workflowId, providerOverride) {
1213
1700
  const workflow = this.workflows.get(workflowId);
1214
1701
  if (!workflow)
1215
1702
  return;
1216
- await this.transitionToPhase(workflowId, 'merging');
1703
+ // Update UI phase for timeline display (FSM is already in 'merging')
1704
+ this.updateUIPhase(workflow, 'merging');
1217
1705
  let executionId;
1218
1706
  try {
1219
1707
  log.info('AI merging changes', { taskId: workflow.taskId }, 'vibing-orchestrator');
@@ -1250,30 +1738,39 @@ export class VibingOrchestrator extends EventEmitter {
1250
1738
  if (!mergeExec || mergeExec.status !== 'completed') {
1251
1739
  throw new Error(`AI merge failed: ${mergeExec?.error || 'Unknown error'}`);
1252
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
+ }
1253
1746
  const latest = this.workflows.get(workflowId) || workflow;
1254
1747
  const timeline = (latest.timeline || []);
1255
1748
  const tMerge = timeline.find((t) => t.executionId === executionId);
1256
1749
  if (tMerge) {
1257
1750
  tMerge.endTime = mergeExec.endTime || new Date().toISOString();
1258
1751
  tMerge.usage = mergeExec.usage;
1752
+ if (mergeVerification.mergeCommit) {
1753
+ tMerge.mergeCommit = mergeVerification.mergeCommit;
1754
+ }
1259
1755
  latest.timeline = this.computeVisibleTimeline(latest);
1260
1756
  latest.lastUpdatedAt = new Date().toISOString();
1261
1757
  await this.saveWorkflow(latest);
1262
1758
  }
1263
- await this.taskService.updateTask(workflow.taskId, { status: 'done' });
1264
- await this.transitionToPhase(workflowId, 'merged');
1265
- const mergedWorkflow = this.workflows.get(workflowId);
1266
- if (mergedWorkflow) {
1267
- mergedWorkflow.status = 'merged';
1268
- mergedWorkflow.lastUpdatedAt = new Date().toISOString();
1269
- 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;
1270
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' });
1271
1770
  if (workflow.metadata.stepByStepMode) {
1272
1771
  log.info('Manual step mode – merge completed; waiting for Continue', { workflowId }, 'vibing-orchestrator');
1273
1772
  }
1274
- else {
1275
- await this.updateCleanupStatus(workflowId);
1276
- }
1773
+ // Note: In non-stepByStepMode, executeCleanup is triggered by reducer's EXECUTE_CLEANUP effect
1277
1774
  log.info('Merge completed, ready for cleanup', { taskId: workflow.taskId }, 'vibing-orchestrator');
1278
1775
  }
1279
1776
  catch (error) {
@@ -1294,21 +1791,98 @@ export class VibingOrchestrator extends EventEmitter {
1294
1791
  await this.handlePhaseFailure(workflowId, 'merging', error);
1295
1792
  }
1296
1793
  }
1297
- async updateCleanupStatus(workflowId) {
1298
- const workflow = this.workflows.get(workflowId);
1299
- if (!workflow)
1300
- return;
1301
- await this.transitionToPhase(workflowId, 'cleaning');
1302
- workflow.status = 'cleaning';
1303
- workflow.lastUpdatedAt = new Date().toISOString();
1304
- workflow.timeline = this.buildTimeline(workflow, {
1305
- id: `cleaning:${new Date().toISOString()}`,
1306
- label: 'Cleaning',
1307
- phase: 'cleaning',
1308
- startTime: new Date().toISOString(),
1309
- });
1310
- await this.saveWorkflow(workflow);
1311
- 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
+ }
1312
1886
  }
1313
1887
  /**
1314
1888
  * Public: run merge phase on demand
@@ -1322,12 +1896,15 @@ export class VibingOrchestrator extends EventEmitter {
1322
1896
  }
1323
1897
  /**
1324
1898
  * Execute cleanup phase: remove worktree/branch and finalize workflow
1899
+ * Note: FSM has already transitioned to 'done' via MERGE_SUCCESS event
1325
1900
  */
1326
1901
  async executeCleanup(workflowId) {
1327
1902
  const workflow = this.workflows.get(workflowId);
1328
1903
  if (!workflow)
1329
1904
  return;
1330
- await this.transitionToPhase(workflowId, 'cleaning');
1905
+ // Update UI phase for timeline display
1906
+ workflow.status = 'cleaning';
1907
+ this.updateUIPhase(workflow, 'cleaning');
1331
1908
  try {
1332
1909
  log.info('Cleaning up resources', { taskId: workflow.taskId }, 'vibing-orchestrator');
1333
1910
  // Remove worktree and local branch if present
@@ -1337,12 +1914,14 @@ export class VibingOrchestrator extends EventEmitter {
1337
1914
  catch (err) {
1338
1915
  log.warn('Cleanup encountered an issue (continuing)', err, 'vibing-orchestrator');
1339
1916
  }
1340
- // Finalize workflow
1917
+ // Finalize workflow - update UI phases
1341
1918
  workflow.status = 'cleaned';
1342
1919
  workflow.endTime = new Date().toISOString();
1343
- 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' });
1344
1923
  workflow.status = 'completed';
1345
- workflow.lastUpdatedAt = new Date().toISOString();
1924
+ this.updateUIPhase(workflow, 'completed');
1346
1925
  await this.saveWorkflow(workflow);
1347
1926
  this.emit('workflowCompleted', workflow);
1348
1927
  log.info('Workflow completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
@@ -1355,9 +1934,13 @@ export class VibingOrchestrator extends EventEmitter {
1355
1934
  error: workflow.error,
1356
1935
  timestamp: new Date().toISOString(),
1357
1936
  };
1358
- await this.transitionToPhase(workflowId, 'failed');
1937
+ // Dispatch failure event
1938
+ await this.dispatch(workflowId, {
1939
+ type: 'CLEAN_FAIL',
1940
+ error: workflow.error,
1941
+ });
1359
1942
  workflow.status = 'failed';
1360
- workflow.lastUpdatedAt = new Date().toISOString();
1943
+ this.updateUIPhase(workflow, 'failed');
1361
1944
  await this.saveWorkflow(workflow);
1362
1945
  this.emit('workflowFailed', workflow);
1363
1946
  }
@@ -1419,11 +2002,13 @@ export class VibingOrchestrator extends EventEmitter {
1419
2002
  const nextPhase = idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : 'completed';
1420
2003
  if (nextPhase && nextPhase !== workflow.phase) {
1421
2004
  log.info('Manually skipping workflow phase', { workflowId, currentPhase: workflow.phase, basePhase, nextPhase }, 'vibing-orchestrator');
1422
- 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);
1423
2008
  // Immediately kick off the next phase where applicable to make skipping faster
1424
2009
  switch (nextPhase) {
1425
2010
  case 'implementing':
1426
- await this.executeImplementation(workflowId);
2011
+ await this.dispatch(workflowId, { type: 'RETRY' });
1427
2012
  break;
1428
2013
  case 'validating':
1429
2014
  await this.executeValidation(workflowId);
@@ -1635,32 +2220,32 @@ export class VibingOrchestrator extends EventEmitter {
1635
2220
  }
1636
2221
  /**
1637
2222
  * Continue workflow execution from current state
2223
+ * Uses the new state machine to determine the next action
1638
2224
  */
1639
2225
  async continueWorkflow(workflowId) {
1640
2226
  const workflow = this.workflows.get(workflowId);
1641
2227
  if (!workflow) {
1642
2228
  throw new Error(`Workflow ${workflowId} not found`);
1643
2229
  }
1644
- log.info('Continuing workflow (status-driven)', { workflowId, phase: workflow.phase, status: workflow.status }, 'vibing-orchestrator');
1645
- // If failed, prefer immediate retry of implementation
1646
- if (workflow.phase === 'failed' || workflow.status === 'failed') {
1647
- if (workflow.metadata.stepByStepMode) {
1648
- await this.rerunImplementation(workflowId);
1649
- }
1650
- else {
1651
- await this.executeImplementation(workflowId);
1652
- }
1653
- return;
1654
- }
1655
- // Compute next actionable phase from status/checkpoint
1656
- const next = this.computeNextActionPhase(workflow);
1657
- if (!next) {
1658
- 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');
1659
2244
  return;
1660
2245
  }
1661
- switch (next) {
2246
+ switch (nextAction.action) {
1662
2247
  case 'implementing':
1663
- await this.executeImplementation(workflowId);
2248
+ await this.executeImplementation(workflowId, nextAction.feedback);
1664
2249
  return;
1665
2250
  case 'validating':
1666
2251
  await this.executeValidation(workflowId);
@@ -1668,56 +2253,17 @@ export class VibingOrchestrator extends EventEmitter {
1668
2253
  case 'ai-reviewing':
1669
2254
  await this.executeAiReviewPhase(workflowId);
1670
2255
  return;
1671
- case 'awaiting-review':
2256
+ case 'awaiting-approval':
1672
2257
  await this.executeAwaitingReview(workflowId);
1673
2258
  return;
1674
2259
  case 'merging':
1675
2260
  await this.executeMerge(workflowId);
1676
2261
  return;
1677
- case 'cleaning':
2262
+ case 'cleanup':
1678
2263
  await this.executeCleanup(workflowId);
1679
2264
  return;
1680
2265
  default:
1681
- throw new Error(`Unsupported next phase computed: ${next}`);
1682
- }
1683
- }
1684
- // Decide next action from status/checkpoint and configuration
1685
- computeNextActionPhase(workflow) {
1686
- const status = workflow.status;
1687
- const cfg = workflow.metadata || {};
1688
- const requireHuman = !!cfg.requireHumanApproval;
1689
- const wantsAIReview = !!cfg.aiCodeReview;
1690
- // If paused, compute by last checkpoint
1691
- // If failed handling is done earlier
1692
- switch (status) {
1693
- case undefined:
1694
- case 'draft':
1695
- return 'implementing';
1696
- case 'implemented':
1697
- return 'validating';
1698
- case 'validated':
1699
- if (wantsAIReview)
1700
- return 'ai-reviewing';
1701
- return requireHuman ? null : 'merging';
1702
- case 'ai-reviewed':
1703
- return requireHuman ? null : 'merging';
1704
- case 'awaiting-review':
1705
- return null; // wait for approveWorkflow
1706
- case 'approved':
1707
- return 'merging';
1708
- case 'merged':
1709
- return 'cleaning';
1710
- case 'cleaned':
1711
- return 'completed';
1712
- case 'completed':
1713
- return null;
1714
- case 'paused':
1715
- // Map paused to next by lastPhase or default to implementing
1716
- return workflow.lastPhase || 'implementing';
1717
- case 'failed':
1718
- return 'implementing';
1719
- default:
1720
- return null;
2266
+ log.warn('Unknown next action from state machine', { workflowId, action: nextAction.action }, 'vibing-orchestrator');
1721
2267
  }
1722
2268
  }
1723
2269
  // ============ Small helpers to reduce duplication ============
@@ -1801,8 +2347,14 @@ export class VibingOrchestrator extends EventEmitter {
1801
2347
  const max = workflow.metadata.retryPolicy?.maxImplementationAttempts || 0;
1802
2348
  const implementingCount = (workflow.phaseHistory || []).filter((h) => h.to === 'implementing').length;
1803
2349
  if (max > 0 && implementingCount < max) {
1804
- await this.transitionToPhase(workflowId, 'implementing');
1805
- 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' });
1806
2358
  return true;
1807
2359
  }
1808
2360
  return false;
@@ -1829,12 +2381,31 @@ export class VibingOrchestrator extends EventEmitter {
1829
2381
  }
1830
2382
  await this.saveWorkflow(wf);
1831
2383
  }
1832
- 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
1833
2390
  workflow.status = 'failed';
1834
- workflow.lastUpdatedAt = new Date().toISOString();
2391
+ this.updateUIPhase(workflow, 'failed');
1835
2392
  await this.saveWorkflow(workflow);
1836
2393
  this.emit('workflowFailed', workflow);
1837
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
+ }
1838
2409
  /**
1839
2410
  * Get the agent service instance
1840
2411
  */