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.
- package/dist/index.js +3 -3
- package/dist/runtime/api/.tsbuildinfo +1 -1
- package/dist/runtime/api/agent/agent-service.d.ts +11 -6
- package/dist/runtime/api/agent/agent-service.js +97 -29
- package/dist/runtime/api/agent/ai-providers/amp-cli-provider.d.ts +38 -0
- package/dist/runtime/api/agent/ai-providers/amp-cli-provider.js +268 -0
- package/dist/runtime/api/agent/ai-providers/codex-cli-provider.d.ts +2 -0
- package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +92 -32
- package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.d.ts +24 -0
- package/dist/runtime/api/agent/ai-providers/gemini-cli-provider.js +291 -0
- package/dist/runtime/api/agent/ai-providers/index.d.ts +3 -3
- package/dist/runtime/api/agent/ai-providers/index.js +3 -1
- package/dist/runtime/api/agent/ai-providers/types.d.ts +5 -2
- package/dist/runtime/api/agent/amp-cli-provider.test.js +99 -0
- package/dist/runtime/api/agent/codex-cli-provider.test.js +54 -7
- package/dist/runtime/api/agent/prompt-service.js +108 -105
- package/dist/runtime/api/agent/prompt-service.test.js +35 -0
- package/dist/runtime/api/agent/routing-policy.d.ts +13 -30
- package/dist/runtime/api/agent/routing-policy.js +82 -132
- package/dist/runtime/api/agent/routing-policy.test.js +63 -0
- package/dist/runtime/api/api/routers/ai.d.ts +15 -3
- package/dist/runtime/api/api/routers/ai.js +7 -6
- package/dist/runtime/api/api/routers/executions.d.ts +3 -8
- package/dist/runtime/api/api/routers/executions.js +2 -2
- package/dist/runtime/api/api/routers/provider-config.d.ts +34 -0
- package/dist/runtime/api/api/routers/settings.d.ts +19 -0
- package/dist/runtime/api/api/routers/settings.js +16 -0
- package/dist/runtime/api/api/routers/tasks.d.ts +10 -10
- package/dist/runtime/api/api/routers/workflows.d.ts +20 -12
- package/dist/runtime/api/api/routers/workflows.js +2 -1
- package/dist/runtime/api/api/routers/worktrees.d.ts +2 -2
- package/dist/runtime/api/api/trpc.d.ts +18 -18
- package/dist/runtime/api/lib/local-config.d.ts +94 -4
- package/dist/runtime/api/lib/local-config.js +16 -0
- package/dist/runtime/api/lib/provider-detection.d.ts +2 -0
- package/dist/runtime/api/lib/provider-detection.js +83 -1
- package/dist/runtime/api/lib/server/vibeman-info.d.ts +5 -0
- package/dist/runtime/api/lib/server/vibeman-info.js +85 -0
- package/dist/runtime/api/lib/trpc/server.d.ts +85 -35
- package/dist/runtime/api/persistence/execution-log-persistence.d.ts +1 -1
- package/dist/runtime/api/persistence/execution-log-persistence.js +19 -3
- package/dist/runtime/api/router.d.ts +85 -35
- package/dist/runtime/api/settings-service.js +70 -5
- package/dist/runtime/api/tasks/task-file-parser.d.ts +1 -0
- package/dist/runtime/api/tasks/task-file-parser.js +20 -1
- package/dist/runtime/api/tasks/task-updater.d.ts +62 -0
- package/dist/runtime/api/tasks/task-updater.js +260 -0
- package/dist/runtime/api/tasks/task-updater.test.d.ts +1 -0
- package/dist/runtime/api/tasks/task-updater.test.js +303 -0
- package/dist/runtime/api/types/index.d.ts +9 -2
- package/dist/runtime/api/types/settings.d.ts +29 -5
- package/dist/runtime/api/vcs/git-service.d.ts +9 -0
- package/dist/runtime/api/vcs/git-service.js +23 -0
- package/dist/runtime/api/vcs/worktree-service.d.ts +1 -1
- package/dist/runtime/api/vcs/worktree-service.js +22 -10
- package/dist/runtime/api/workflows/quality-pipeline.js +2 -1
- package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +93 -5
- package/dist/runtime/api/workflows/vibing-orchestrator.js +806 -204
- package/dist/runtime/api/workflows/workflow-effects.d.ts +45 -0
- package/dist/runtime/api/workflows/workflow-effects.js +49 -0
- package/dist/runtime/api/workflows/workflow-reconciler.d.ts +65 -0
- package/dist/runtime/api/workflows/workflow-reconciler.js +226 -0
- package/dist/runtime/api/workflows/workflow-reducer.d.ts +26 -0
- package/dist/runtime/api/workflows/workflow-reducer.js +288 -0
- package/dist/runtime/api/workflows/workflow-reducer.test.d.ts +1 -0
- package/dist/runtime/api/workflows/workflow-reducer.test.js +247 -0
- package/dist/runtime/api/workflows/workflow-schema.d.ts +546 -0
- package/dist/runtime/api/workflows/workflow-schema.js +256 -0
- package/dist/runtime/web/.next/BUILD_ID +1 -1
- package/dist/runtime/web/.next/app-build-manifest.json +51 -44
- package/dist/runtime/web/.next/app-path-routes-manifest.json +2 -1
- package/dist/runtime/web/.next/build-manifest.json +14 -14
- package/dist/runtime/web/.next/prerender-manifest.json +10 -10
- package/dist/runtime/web/.next/react-loadable-manifest.json +2 -33
- package/dist/runtime/web/.next/required-server-files.json +5 -5
- package/dist/runtime/web/.next/routes-manifest.json +8 -0
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +1 -0
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -0
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -0
- package/dist/runtime/web/.next/server/app/_not-found/page.js +2 -2
- package/dist/runtime/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
- package/dist/runtime/web/.next/server/app/_not-found.rsc +12 -12
- package/dist/runtime/web/.next/server/app/api/health/route.js +1 -1
- package/dist/runtime/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +1 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/api/upload/route.js +1 -1
- package/dist/runtime/web/.next/server/app/api/upload/route.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/index.html +2 -2
- package/dist/runtime/web/.next/server/app/index.rsc +15 -15
- package/dist/runtime/web/.next/server/app/page.js +27 -62
- package/dist/runtime/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app-paths-manifest.json +2 -1
- package/dist/runtime/web/.next/server/chunks/210.js +1 -0
- package/dist/runtime/web/.next/server/chunks/291.js +18 -0
- package/dist/runtime/web/.next/server/chunks/552.js +22 -0
- package/dist/runtime/web/.next/server/chunks/780.js +1 -0
- package/dist/runtime/web/.next/server/chunks/905.js +6 -0
- package/dist/runtime/web/.next/server/chunks/98.js +1 -0
- package/dist/runtime/web/.next/server/middleware-build-manifest.js +1 -1
- package/dist/runtime/web/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/runtime/web/.next/server/pages/404.html +2 -2
- package/dist/runtime/web/.next/server/pages/500.html +1 -1
- package/dist/runtime/web/.next/server/pages/_app.js +1 -1
- package/dist/runtime/web/.next/server/pages/_app.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/pages/_document.js +1 -1
- package/dist/runtime/web/.next/server/pages/_document.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/pages/_error.js +9 -9
- package/dist/runtime/web/.next/server/pages/_error.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/pages-manifest.json +1 -1
- package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/runtime/web/.next/server/webpack-runtime.js +1 -1
- package/dist/runtime/web/.next/static/LJFZk_8tvKFN_Ee4HqUuM/_buildManifest.js +1 -0
- package/dist/runtime/web/.next/static/chunks/05c91ade-7d09b2b280adffd1.js +1 -0
- package/dist/runtime/web/.next/static/chunks/201-51bef3fa8c832e2e.js +1 -0
- package/dist/runtime/web/.next/static/chunks/524-89747ed9b0294f8a.js +1 -0
- package/dist/runtime/web/.next/static/chunks/554-8bec6e9cca6acc67.js +1 -0
- package/dist/runtime/web/.next/static/chunks/764.86e9503a69d45a85.js +1 -0
- package/dist/runtime/web/.next/static/chunks/{87c73c54-09e1ba5c70e60a51.js → 7ab4dc20-239138e0ae7af24a.js} +1 -1
- package/dist/runtime/web/.next/static/chunks/905-342391e3d3a3678f.js +20 -0
- package/dist/runtime/web/.next/static/chunks/a8a5ce16-4edea7df2d9b544a.js +79 -0
- package/dist/runtime/web/.next/static/chunks/{8bb4d8db-3e2aa02b0a2384b9.js → ad74d572-4c1b162e2c15acaa.js} +1 -1
- package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-7b752a8641f96c1f.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/_not-found/page-34e66b251c2b5044.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/api/health/route-7b752a8641f96c1f.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-7b752a8641f96c1f.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/api/upload/route-7b752a8641f96c1f.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/layout-df9ac93cb02b2385.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/page-6610743f7de5f92a.js +1 -0
- package/dist/runtime/web/.next/static/chunks/c25e0690-e9b798b8de667da1.js +1 -0
- package/dist/runtime/web/.next/static/chunks/framework-57157ec4d37f64aa.js +1 -0
- package/dist/runtime/web/.next/static/chunks/main-app-156cc0c60371bd78.js +1 -0
- package/dist/runtime/web/.next/static/chunks/main-df25d367c47b1fec.js +1 -0
- package/dist/runtime/web/.next/static/chunks/pages/_app-9f629a5e1131d19f.js +1 -0
- package/dist/runtime/web/.next/static/chunks/pages/_error-9238238274c7efcd.js +1 -0
- package/dist/runtime/web/.next/static/chunks/webpack-cd50e39b423d1808.js +1 -0
- package/dist/runtime/web/.next/static/css/4fbf378a264bd4ea.css +1 -0
- package/dist/runtime/web/package.json +8 -8
- package/dist/runtime/web/server.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -37
- package/dist/runtime/api/lib/image-paste-drop-extension.d.ts +0 -26
- package/dist/runtime/api/lib/image-paste-drop-extension.js +0 -125
- package/dist/runtime/api/lib/markdown-utils.d.ts +0 -8
- package/dist/runtime/api/lib/markdown-utils.js +0 -282
- package/dist/runtime/api/lib/markdown-utils.test.js +0 -348
- package/dist/runtime/api/lib/tiptap-utils.clamp-selection.test.js +0 -27
- package/dist/runtime/api/lib/tiptap-utils.d.ts +0 -130
- package/dist/runtime/api/lib/tiptap-utils.js +0 -327
- package/dist/runtime/api/lib/trpc/client.d.ts +0 -1
- package/dist/runtime/api/lib/trpc/client.js +0 -5
- package/dist/runtime/web/.next/server/chunks/217.js +0 -1
- package/dist/runtime/web/.next/server/chunks/383.js +0 -6
- package/dist/runtime/web/.next/server/chunks/458.js +0 -1
- package/dist/runtime/web/.next/server/chunks/576.js +0 -18
- package/dist/runtime/web/.next/server/chunks/635.js +0 -22
- package/dist/runtime/web/.next/server/chunks/761.js +0 -1
- package/dist/runtime/web/.next/server/chunks/777.js +0 -3
- package/dist/runtime/web/.next/server/chunks/825.js +0 -1
- package/dist/runtime/web/.next/server/chunks/838.js +0 -1
- package/dist/runtime/web/.next/server/chunks/973.js +0 -15
- package/dist/runtime/web/.next/static/chunks/18-15c10d3288afef2e.js +0 -1
- package/dist/runtime/web/.next/static/chunks/1c0ca389.537bbe362e3ffbd9.js +0 -3
- package/dist/runtime/web/.next/static/chunks/22747d63-ad5da0c19f4cfe41.js +0 -71
- package/dist/runtime/web/.next/static/chunks/277-0142a939f08738c3.js +0 -63
- package/dist/runtime/web/.next/static/chunks/355.056c2645878a799a.js +0 -1
- package/dist/runtime/web/.next/static/chunks/420.a5ccf151c9e2b2f1.js +0 -1
- package/dist/runtime/web/.next/static/chunks/439.1be0c6242fd248d5.js +0 -15
- package/dist/runtime/web/.next/static/chunks/440.c52e7c0f797e22b2.js +0 -1
- package/dist/runtime/web/.next/static/chunks/575-e2478287c27da87b.js +0 -1
- package/dist/runtime/web/.next/static/chunks/691.920d88c115087314.js +0 -1
- package/dist/runtime/web/.next/static/chunks/765-e838910065b50c3d.js +0 -1
- package/dist/runtime/web/.next/static/chunks/891cff7f.0f71fc028f87e683.js +0 -1
- package/dist/runtime/web/.next/static/chunks/9af238c7-271a911d4e99ab18.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/_not-found/page-1cb74d1cba27d0ab.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/health/route-105a61ae865ba536.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-105a61ae865ba536.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/upload/route-105a61ae865ba536.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/layout-8435322f09fd0975.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/page-8c3ba579efc6f918.js +0 -1
- package/dist/runtime/web/.next/static/chunks/cac567b0-5b77dd12911823cd.js +0 -1
- package/dist/runtime/web/.next/static/chunks/framework-2518f1345b5b2806.js +0 -1
- package/dist/runtime/web/.next/static/chunks/main-17665e5e39de9a8a.js +0 -1
- package/dist/runtime/web/.next/static/chunks/main-app-c0b0f5ba4f7f9d75.js +0 -1
- package/dist/runtime/web/.next/static/chunks/pages/_app-d6f6b3bbc3d81ee1.js +0 -1
- package/dist/runtime/web/.next/static/chunks/pages/_error-75a96cf1997cc3b9.js +0 -1
- package/dist/runtime/web/.next/static/chunks/webpack-c8de37305b4635cf.js +0 -1
- package/dist/runtime/web/.next/static/css/08c950681f1a9a92.css +0 -1
- package/dist/runtime/web/.next/static/mRpNgPfbYR_0wrODzlg_4/_buildManifest.js +0 -1
- /package/dist/runtime/api/{lib/markdown-utils.test.d.ts → agent/amp-cli-provider.test.d.ts} +0 -0
- /package/dist/runtime/api/{lib/tiptap-utils.clamp-selection.test.d.ts → agent/routing-policy.test.d.ts} +0 -0
- /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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
478
|
-
await this.
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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 ===
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
802
|
-
|
|
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
|
-
|
|
831
|
-
await this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
workflow
|
|
1163
|
-
await this.
|
|
1164
|
-
if (workflow.metadata.
|
|
1165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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 (
|
|
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-
|
|
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 '
|
|
2262
|
+
case 'cleanup':
|
|
1655
2263
|
await this.executeCleanup(workflowId);
|
|
1656
2264
|
return;
|
|
1657
2265
|
default:
|
|
1658
|
-
|
|
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
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|