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.
- package/dist/index.js +3 -3
- package/dist/runtime/api/.tsbuildinfo +1 -1
- package/dist/runtime/api/agent/agent-service.d.ts +4 -0
- package/dist/runtime/api/agent/agent-service.js +62 -3
- 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.js +40 -12
- 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.d.ts +1 -0
- package/dist/runtime/api/agent/amp-cli-provider.test.js +99 -0
- package/dist/runtime/api/agent/codex-cli-provider.test.js +10 -8
- 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 +2 -2
- package/dist/runtime/api/agent/routing-policy.test.js +4 -4
- package/dist/runtime/api/api/routers/ai.d.ts +3 -3
- package/dist/runtime/api/api/routers/executions.d.ts +2 -7
- 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 +9 -9
- package/dist/runtime/api/api/routers/workflows.d.ts +12 -12
- package/dist/runtime/api/api/routers/worktrees.d.ts +2 -2
- package/dist/runtime/api/api/trpc.d.ts +16 -16
- 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 +63 -33
- 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 +63 -33
- package/dist/runtime/api/settings-service.js +31 -14
- 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 +1 -1
- package/dist/runtime/api/types/settings.d.ts +17 -6
- 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 +774 -203
- 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 +50 -50
- package/dist/runtime/web/.next/app-path-routes-manifest.json +1 -1
- package/dist/runtime/web/.next/build-manifest.json +14 -14
- package/dist/runtime/web/.next/prerender-manifest.json +3 -3
- 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/server/app/.vibeman/assets/images/[...path]/route.js +1 -1
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -1
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -1
- 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 +1 -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/{5_15u1WQCxN1_eHZpldCv → LJFZk_8tvKFN_Ee4HqUuM}/_buildManifest.js +1 -1
- 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/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/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/823-6f371a6e829adbba.js +0 -63
- 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/.vibeman/assets/images/[...path]/route-751c9265a65409e5.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-751c9265a65409e5.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-751c9265a65409e5.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/upload/route-751c9265a65409e5.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-9fe7d75095b4ccec.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/{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
|
-
|
|
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,
|
|
@@ -476,11 +898,9 @@ export class VibingOrchestrator extends EventEmitter {
|
|
|
476
898
|
...workflow.metadata,
|
|
477
899
|
humanCompleted: true,
|
|
478
900
|
};
|
|
479
|
-
|
|
480
|
-
await this.
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 ===
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
813
|
-
|
|
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
|
-
|
|
842
|
-
await this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
workflow
|
|
1186
|
-
await this.
|
|
1187
|
-
if (workflow.metadata.
|
|
1188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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 (
|
|
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-
|
|
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 '
|
|
2262
|
+
case 'cleanup':
|
|
1678
2263
|
await this.executeCleanup(workflowId);
|
|
1679
2264
|
return;
|
|
1680
2265
|
default:
|
|
1681
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
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
|
|
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
|
*/
|