vibeman 0.0.3 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +49 -0
- package/dist/cli.js +135 -0
- package/dist/ui/index-gnk6rhxs.js +9 -0
- package/dist/ui/index.html +10 -0
- package/dist/ui/index.js +2 -0
- package/package.json +10 -80
- package/README.md +0 -12
- package/dist/index.js +0 -114
- package/dist/runtime/api/.tsbuildinfo +0 -1
- package/dist/runtime/api/agent/agent-service.d.ts +0 -225
- package/dist/runtime/api/agent/agent-service.js +0 -904
- package/dist/runtime/api/agent/ai-providers/claude-code-adapter.d.ts +0 -61
- package/dist/runtime/api/agent/ai-providers/claude-code-adapter.js +0 -362
- package/dist/runtime/api/agent/ai-providers/codex-cli-provider.d.ts +0 -36
- package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +0 -347
- package/dist/runtime/api/agent/ai-providers/index.d.ts +0 -9
- package/dist/runtime/api/agent/ai-providers/index.js +0 -7
- package/dist/runtime/api/agent/ai-providers/types.d.ts +0 -182
- package/dist/runtime/api/agent/ai-providers/types.js +0 -5
- package/dist/runtime/api/agent/codex-cli-provider.test.d.ts +0 -1
- package/dist/runtime/api/agent/codex-cli-provider.test.js +0 -170
- package/dist/runtime/api/agent/core-agent-service.d.ts +0 -119
- package/dist/runtime/api/agent/core-agent-service.js +0 -267
- package/dist/runtime/api/agent/parsers.d.ts +0 -16
- package/dist/runtime/api/agent/parsers.js +0 -308
- package/dist/runtime/api/agent/prompt-service.d.ts +0 -30
- package/dist/runtime/api/agent/prompt-service.js +0 -449
- package/dist/runtime/api/agent/prompt-service.test.d.ts +0 -1
- package/dist/runtime/api/agent/prompt-service.test.js +0 -230
- package/dist/runtime/api/agent/routing-policy.d.ts +0 -171
- package/dist/runtime/api/agent/routing-policy.js +0 -196
- package/dist/runtime/api/agent/routing-policy.test.d.ts +0 -1
- package/dist/runtime/api/agent/routing-policy.test.js +0 -63
- package/dist/runtime/api/api/router-helpers.d.ts +0 -32
- package/dist/runtime/api/api/router-helpers.js +0 -31
- package/dist/runtime/api/api/routers/ai.d.ts +0 -200
- package/dist/runtime/api/api/routers/ai.js +0 -396
- package/dist/runtime/api/api/routers/executions.d.ts +0 -98
- package/dist/runtime/api/api/routers/executions.js +0 -94
- package/dist/runtime/api/api/routers/git.d.ts +0 -45
- package/dist/runtime/api/api/routers/git.js +0 -35
- package/dist/runtime/api/api/routers/provider-config.d.ts +0 -165
- package/dist/runtime/api/api/routers/provider-config.js +0 -252
- package/dist/runtime/api/api/routers/settings.d.ts +0 -139
- package/dist/runtime/api/api/routers/settings.js +0 -113
- package/dist/runtime/api/api/routers/tasks.d.ts +0 -141
- package/dist/runtime/api/api/routers/tasks.js +0 -238
- package/dist/runtime/api/api/routers/workflows.d.ts +0 -275
- package/dist/runtime/api/api/routers/workflows.js +0 -311
- package/dist/runtime/api/api/routers/worktrees.d.ts +0 -101
- package/dist/runtime/api/api/routers/worktrees.js +0 -80
- package/dist/runtime/api/api/trpc.d.ts +0 -118
- package/dist/runtime/api/api/trpc.js +0 -34
- package/dist/runtime/api/index.d.ts +0 -9
- package/dist/runtime/api/index.js +0 -117
- package/dist/runtime/api/lib/id-generator.d.ts +0 -70
- package/dist/runtime/api/lib/id-generator.js +0 -123
- package/dist/runtime/api/lib/local-config.d.ts +0 -245
- package/dist/runtime/api/lib/local-config.js +0 -288
- package/dist/runtime/api/lib/logger.d.ts +0 -11
- package/dist/runtime/api/lib/logger.js +0 -188
- package/dist/runtime/api/lib/provider-detection.d.ts +0 -59
- package/dist/runtime/api/lib/provider-detection.js +0 -244
- package/dist/runtime/api/lib/server/agent-service-singleton.d.ts +0 -6
- package/dist/runtime/api/lib/server/agent-service-singleton.js +0 -27
- package/dist/runtime/api/lib/server/bootstrap.d.ts +0 -38
- package/dist/runtime/api/lib/server/bootstrap.js +0 -197
- package/dist/runtime/api/lib/server/git-service-singleton.d.ts +0 -6
- package/dist/runtime/api/lib/server/git-service-singleton.js +0 -47
- package/dist/runtime/api/lib/server/project-root.d.ts +0 -2
- package/dist/runtime/api/lib/server/project-root.js +0 -61
- package/dist/runtime/api/lib/server/task-service-singleton.d.ts +0 -7
- package/dist/runtime/api/lib/server/task-service-singleton.js +0 -58
- package/dist/runtime/api/lib/server/vibing-orchestrator-singleton.d.ts +0 -7
- package/dist/runtime/api/lib/server/vibing-orchestrator-singleton.js +0 -57
- package/dist/runtime/api/lib/trpc/client.d.ts +0 -1
- package/dist/runtime/api/lib/trpc/client.js +0 -5
- package/dist/runtime/api/lib/trpc/server.d.ts +0 -935
- package/dist/runtime/api/lib/trpc/server.js +0 -11
- package/dist/runtime/api/lib/trpc/ws-server.d.ts +0 -8
- package/dist/runtime/api/lib/trpc/ws-server.js +0 -33
- package/dist/runtime/api/persistence/database-service.d.ts +0 -14
- package/dist/runtime/api/persistence/database-service.js +0 -74
- package/dist/runtime/api/persistence/execution-log-persistence.d.ts +0 -90
- package/dist/runtime/api/persistence/execution-log-persistence.js +0 -410
- package/dist/runtime/api/persistence/execution-log-persistence.test.d.ts +0 -1
- package/dist/runtime/api/persistence/execution-log-persistence.test.js +0 -170
- package/dist/runtime/api/router.d.ts +0 -938
- package/dist/runtime/api/router.js +0 -34
- package/dist/runtime/api/settings-service.d.ts +0 -110
- package/dist/runtime/api/settings-service.js +0 -661
- package/dist/runtime/api/tasks/file-watcher.d.ts +0 -23
- package/dist/runtime/api/tasks/file-watcher.js +0 -88
- package/dist/runtime/api/tasks/task-file-parser.d.ts +0 -13
- package/dist/runtime/api/tasks/task-file-parser.js +0 -161
- package/dist/runtime/api/tasks/task-service.d.ts +0 -36
- package/dist/runtime/api/tasks/task-service.js +0 -173
- package/dist/runtime/api/types/index.d.ts +0 -186
- package/dist/runtime/api/types/index.js +0 -1
- package/dist/runtime/api/types/settings.d.ts +0 -94
- package/dist/runtime/api/types/settings.js +0 -2
- package/dist/runtime/api/types.d.ts +0 -2
- package/dist/runtime/api/types.js +0 -1
- package/dist/runtime/api/utils/env.d.ts +0 -6
- package/dist/runtime/api/utils/env.js +0 -12
- package/dist/runtime/api/utils/stripNextEnv.d.ts +0 -7
- package/dist/runtime/api/utils/stripNextEnv.js +0 -22
- package/dist/runtime/api/utils/title-slug.d.ts +0 -6
- package/dist/runtime/api/utils/title-slug.js +0 -77
- package/dist/runtime/api/utils/url.d.ts +0 -2
- package/dist/runtime/api/utils/url.js +0 -19
- package/dist/runtime/api/vcs/git-history-service.d.ts +0 -57
- package/dist/runtime/api/vcs/git-history-service.js +0 -228
- package/dist/runtime/api/vcs/git-service.d.ts +0 -127
- package/dist/runtime/api/vcs/git-service.js +0 -284
- package/dist/runtime/api/vcs/worktree-service.d.ts +0 -93
- package/dist/runtime/api/vcs/worktree-service.js +0 -506
- package/dist/runtime/api/vcs/worktree-service.test.d.ts +0 -1
- package/dist/runtime/api/vcs/worktree-service.test.js +0 -20
- package/dist/runtime/api/workflows/quality-pipeline.d.ts +0 -58
- package/dist/runtime/api/workflows/quality-pipeline.js +0 -400
- package/dist/runtime/api/workflows/vibing-orchestrator.d.ts +0 -318
- package/dist/runtime/api/workflows/vibing-orchestrator.js +0 -1891
- package/dist/runtime/web/.next/BUILD_ID +0 -1
- package/dist/runtime/web/.next/app-build-manifest.json +0 -66
- package/dist/runtime/web/.next/app-path-routes-manifest.json +0 -8
- package/dist/runtime/web/.next/build-manifest.json +0 -33
- package/dist/runtime/web/.next/package.json +0 -1
- package/dist/runtime/web/.next/prerender-manifest.json +0 -61
- package/dist/runtime/web/.next/react-loadable-manifest.json +0 -39
- package/dist/runtime/web/.next/required-server-files.json +0 -334
- package/dist/runtime/web/.next/routes-manifest.json +0 -70
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +0 -1
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/app/_not-found/page.js +0 -2
- package/dist/runtime/web/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/app/_not-found.html +0 -7
- package/dist/runtime/web/.next/server/app/_not-found.meta +0 -8
- package/dist/runtime/web/.next/server/app/_not-found.rsc +0 -22
- package/dist/runtime/web/.next/server/app/api/health/route.js +0 -1
- package/dist/runtime/web/.next/server/app/api/health/route.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +0 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/app/api/upload/route.js +0 -1
- package/dist/runtime/web/.next/server/app/api/upload/route.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/app/index.html +0 -7
- package/dist/runtime/web/.next/server/app/index.meta +0 -7
- package/dist/runtime/web/.next/server/app/index.rsc +0 -27
- package/dist/runtime/web/.next/server/app/page.js +0 -147
- package/dist/runtime/web/.next/server/app/page.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/app-paths-manifest.json +0 -8
- 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/server/functions-config-manifest.json +0 -4
- package/dist/runtime/web/.next/server/middleware-build-manifest.js +0 -1
- package/dist/runtime/web/.next/server/middleware-manifest.json +0 -6
- package/dist/runtime/web/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/dist/runtime/web/.next/server/next-font-manifest.js +0 -1
- package/dist/runtime/web/.next/server/next-font-manifest.json +0 -1
- package/dist/runtime/web/.next/server/pages/404.html +0 -7
- package/dist/runtime/web/.next/server/pages/500.html +0 -1
- package/dist/runtime/web/.next/server/pages/_app.js +0 -1
- package/dist/runtime/web/.next/server/pages/_app.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/pages/_document.js +0 -1
- package/dist/runtime/web/.next/server/pages/_document.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/pages/_error.js +0 -19
- package/dist/runtime/web/.next/server/pages/_error.js.nft.json +0 -1
- package/dist/runtime/web/.next/server/pages-manifest.json +0 -6
- package/dist/runtime/web/.next/server/server-reference-manifest.js +0 -1
- package/dist/runtime/web/.next/server/server-reference-manifest.json +0 -1
- package/dist/runtime/web/.next/server/webpack-runtime.js +0 -1
- package/dist/runtime/web/.next/static/5_15u1WQCxN1_eHZpldCv/_buildManifest.js +0 -1
- package/dist/runtime/web/.next/static/5_15u1WQCxN1_eHZpldCv/_ssgManifest.js +0 -1
- 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/87c73c54-09e1ba5c70e60a51.js +0 -1
- package/dist/runtime/web/.next/static/chunks/891cff7f.0f71fc028f87e683.js +0 -1
- package/dist/runtime/web/.next/static/chunks/8bb4d8db-3e2aa02b0a2384b9.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/polyfills-42372ed130431b0a.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/css/2728291c68f99cb1.css +0 -3
- package/dist/runtime/web/.next/static/css/521bd69cc298cd1a.css +0 -1
- package/dist/runtime/web/.next/static/css/537e22821e101b87.css +0 -1
- package/dist/runtime/web/.next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/dist/runtime/web/.next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/dist/runtime/web/.next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/dist/runtime/web/.next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/dist/runtime/web/.next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/dist/runtime/web/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/dist/runtime/web/.next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/dist/runtime/web/package.json +0 -65
- package/dist/runtime/web/server.js +0 -44
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,1891 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { QualityPipeline } from './quality-pipeline.js';
|
|
3
|
-
import { DatabaseService } from '../persistence/database-service.js';
|
|
4
|
-
import { log } from '../lib/logger.js';
|
|
5
|
-
import { generateId } from '../lib/id-generator.js';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { stripNextInjectedEnv } from '../utils/stripNextEnv.js';
|
|
8
|
-
import { spawn } from 'child_process';
|
|
9
|
-
import { getSettingsService } from '../settings-service.js';
|
|
10
|
-
import { getVibeDir } from '../lib/server/project-root.js';
|
|
11
|
-
// Canonical phase flow and labels for server-driven timeline and placeholders
|
|
12
|
-
// Action phases only; placeholders are created for these
|
|
13
|
-
const PHASE_FLOW = [
|
|
14
|
-
'implementing',
|
|
15
|
-
'validating',
|
|
16
|
-
'ai-reviewing',
|
|
17
|
-
// Show awaiting-review explicitly in the main flow so the timeline
|
|
18
|
-
// reflects the human approval step in order
|
|
19
|
-
'awaiting-review',
|
|
20
|
-
'merging',
|
|
21
|
-
'cleaning',
|
|
22
|
-
];
|
|
23
|
-
const PHASE_LABELS = {
|
|
24
|
-
draft: 'Workflow started',
|
|
25
|
-
implementing: 'Implementation',
|
|
26
|
-
implemented: 'Implemented',
|
|
27
|
-
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
|
-
merging: 'Merge',
|
|
35
|
-
merged: 'Merged',
|
|
36
|
-
cleaning: 'Cleaning',
|
|
37
|
-
cleaned: 'Cleaned',
|
|
38
|
-
completed: 'Completed',
|
|
39
|
-
failed: 'Failed',
|
|
40
|
-
};
|
|
41
|
-
// Default config
|
|
42
|
-
const getDefaultConfig = async () => {
|
|
43
|
-
try {
|
|
44
|
-
const settingsService = getSettingsService();
|
|
45
|
-
await settingsService.initialize();
|
|
46
|
-
const settings = settingsService.getSettings();
|
|
47
|
-
return {
|
|
48
|
-
autoQualityChecks: settings.defaultWorkflow.autoQualityChecks,
|
|
49
|
-
requireHumanApproval: settings.defaultWorkflow.requireHumanApproval,
|
|
50
|
-
aiCodeReview: settings.defaultWorkflow.aiCodeReview,
|
|
51
|
-
stepByStepMode: false,
|
|
52
|
-
retryPolicy: {
|
|
53
|
-
maxImplementationAttempts: settings.defaultWorkflow.maxImplementationAttempts,
|
|
54
|
-
},
|
|
55
|
-
autoCommit: settings.defaultWorkflow.autoCommit,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
log.warn('Failed to load settings, using fallback defaults', error, 'vibing-orchestrator');
|
|
60
|
-
return {
|
|
61
|
-
autoQualityChecks: true,
|
|
62
|
-
requireHumanApproval: true,
|
|
63
|
-
aiCodeReview: true,
|
|
64
|
-
stepByStepMode: false,
|
|
65
|
-
retryPolicy: { maxImplementationAttempts: 3 },
|
|
66
|
-
autoCommit: false,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
export class VibingOrchestrator extends EventEmitter {
|
|
71
|
-
// Build a new, visible timeline array without mutating the input
|
|
72
|
-
// Correct order: remove placeholders -> add real phases -> fill missing placeholders
|
|
73
|
-
computeVisibleTimeline(wf) {
|
|
74
|
-
const base = Array.isArray(wf.timeline) ? [...wf.timeline] : [];
|
|
75
|
-
// Visible phases in timeline
|
|
76
|
-
const visible = new Set([
|
|
77
|
-
'draft',
|
|
78
|
-
'implementing',
|
|
79
|
-
'validating',
|
|
80
|
-
'ai-reviewing',
|
|
81
|
-
'awaiting-review',
|
|
82
|
-
'merging',
|
|
83
|
-
'cleaning',
|
|
84
|
-
'failed',
|
|
85
|
-
'paused',
|
|
86
|
-
]);
|
|
87
|
-
// Ensure a start marker exists (non-placeholder)
|
|
88
|
-
let startItem = base.find((t) => t.phase === 'draft' && !t.placeholder);
|
|
89
|
-
if (!startItem && wf.startTime) {
|
|
90
|
-
startItem = {
|
|
91
|
-
id: `start:${wf.startTime}`,
|
|
92
|
-
label: 'Workflow started',
|
|
93
|
-
phase: 'draft',
|
|
94
|
-
startTime: wf.startTime,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
// Remove all placeholders and keep only visible phases
|
|
98
|
-
const realItems = base.filter((t) => !t.placeholder && visible.has(t.phase));
|
|
99
|
-
// Group real items by phase (preserve per-phase chronological order)
|
|
100
|
-
// TODO: retry logic is different from grouping by phase
|
|
101
|
-
const realByPhase = new Map();
|
|
102
|
-
for (const item of realItems) {
|
|
103
|
-
const ph = item.phase;
|
|
104
|
-
if (!realByPhase.has(ph))
|
|
105
|
-
realByPhase.set(ph, []);
|
|
106
|
-
realByPhase.get(ph).push(item);
|
|
107
|
-
}
|
|
108
|
-
for (const [, items] of realByPhase) {
|
|
109
|
-
items.sort((a, b) => {
|
|
110
|
-
const ta = new Date(a.startTime || wf.startTime || 0).getTime();
|
|
111
|
-
const tb = new Date(b.startTime || wf.startTime || 0).getTime();
|
|
112
|
-
return ta - tb;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
// Compose final ordered list: start -> each phase in PHASE_FLOW (real items if any, else placeholder)
|
|
116
|
-
const out = [];
|
|
117
|
-
if (startItem)
|
|
118
|
-
out.push(startItem);
|
|
119
|
-
const ts = wf.metrics?.lastPhaseStartedAt ||
|
|
120
|
-
wf.lastUpdatedAt ||
|
|
121
|
-
wf.startTime ||
|
|
122
|
-
new Date().toISOString();
|
|
123
|
-
for (const ph of PHASE_FLOW) {
|
|
124
|
-
const items = realByPhase.get(ph) || [];
|
|
125
|
-
if (items.length > 0) {
|
|
126
|
-
out.push(...items);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
out.push({
|
|
130
|
-
id: `placeholder:${ph}`,
|
|
131
|
-
label: PHASE_LABELS[ph],
|
|
132
|
-
phase: ph,
|
|
133
|
-
startTime: ts,
|
|
134
|
-
placeholder: true,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// Append any other visible real items not in PHASE_FLOW (e.g., 'awaiting-review', 'failed', 'paused')
|
|
139
|
-
for (const [ph, items] of realByPhase) {
|
|
140
|
-
if (!PHASE_FLOW.includes(ph) && ph !== 'draft') {
|
|
141
|
-
out.push(...items);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
146
|
-
// Build a new timeline by appending an optional item, then normalizing
|
|
147
|
-
buildTimeline(wf, newItem) {
|
|
148
|
-
const base = Array.isArray(wf.timeline) ? [...wf.timeline] : [];
|
|
149
|
-
if (newItem)
|
|
150
|
-
base.push(newItem);
|
|
151
|
-
const tmp = { ...wf, timeline: base };
|
|
152
|
-
return this.computeVisibleTimeline(tmp);
|
|
153
|
-
}
|
|
154
|
-
constructor(taskService, agentService, worktreeService, config, gitService, persistenceDataDir) {
|
|
155
|
-
super();
|
|
156
|
-
this.taskService = taskService;
|
|
157
|
-
this.agentService = agentService;
|
|
158
|
-
this.worktreeService = worktreeService;
|
|
159
|
-
this.config = config;
|
|
160
|
-
this.gitService = gitService;
|
|
161
|
-
this.workflows = new Map();
|
|
162
|
-
this.initialized = false;
|
|
163
|
-
this.qualityPipeline = new QualityPipeline();
|
|
164
|
-
// Persist workflows in a JSON file using the generic database service
|
|
165
|
-
const filePath = persistenceDataDir
|
|
166
|
-
? path.join(persistenceDataDir, 'workflows.json')
|
|
167
|
-
: path.join(getVibeDir(), 'workflows.json');
|
|
168
|
-
this.workflowDB = new DatabaseService(filePath);
|
|
169
|
-
// Forward execution updates from AgentService so UIs can subscribe via orchestrator
|
|
170
|
-
this.agentService.on('executionUpdated', (update) => {
|
|
171
|
-
this.emit('executionUpdated', update);
|
|
172
|
-
});
|
|
173
|
-
// Listen for execution creation so we can associate executionId to workflows
|
|
174
|
-
this.agentService.on('executionCreated', async (data) => {
|
|
175
|
-
try {
|
|
176
|
-
const wfId = data.workflowId;
|
|
177
|
-
if (!wfId)
|
|
178
|
-
return;
|
|
179
|
-
const wf = this.workflows.get(wfId);
|
|
180
|
-
if (!wf)
|
|
181
|
-
return;
|
|
182
|
-
// Track execution IDs and attempts for implementing phase
|
|
183
|
-
const attempt = (wf.attempts?.implementing || 0) + 1;
|
|
184
|
-
const executionIds = (wf.executionIds ?? (wf.executionIds = {}));
|
|
185
|
-
const implList = (executionIds['implementing'] || []);
|
|
186
|
-
executionIds['implementing'] = [...implList, data.executionId];
|
|
187
|
-
wf.attempts = wf.attempts || {};
|
|
188
|
-
wf.attempts['implementing'] = attempt;
|
|
189
|
-
// Timeline: record implementation attempt start
|
|
190
|
-
wf.timeline = this.buildTimeline(wf, {
|
|
191
|
-
id: data.executionId,
|
|
192
|
-
label: attempt > 1 ? `Implementation – Retry #${attempt}` : 'Implementation',
|
|
193
|
-
phase: 'implementing',
|
|
194
|
-
attempt,
|
|
195
|
-
executionId: data.executionId,
|
|
196
|
-
startTime: new Date().toISOString(),
|
|
197
|
-
});
|
|
198
|
-
await this.saveWorkflow(wf);
|
|
199
|
-
// Notify listeners that an execution for this workflow has started
|
|
200
|
-
this.emit('workflowExecutionStarted', {
|
|
201
|
-
workflowId: wfId,
|
|
202
|
-
executionId: data.executionId,
|
|
203
|
-
phase: 'implementing',
|
|
204
|
-
workflow: wf,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
log.warn('Failed to handle executionCreated event', err, 'vibing-orchestrator');
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Initialize workflow orchestrator by loading existing workflows
|
|
214
|
-
* Call this after construction to load persisted workflows
|
|
215
|
-
*/
|
|
216
|
-
async initialize() {
|
|
217
|
-
try {
|
|
218
|
-
// Load existing workflows from database
|
|
219
|
-
const all = await this.workflowDB.getAll();
|
|
220
|
-
const map = new Map();
|
|
221
|
-
for (const [id, wf] of Object.entries(all)) {
|
|
222
|
-
map.set(id, this.normalizeWorkflow(wf));
|
|
223
|
-
}
|
|
224
|
-
this.workflows = map;
|
|
225
|
-
log.debug('Loaded existing workflows from disk', { count: this.workflows.size }, 'vibing-orchestrator');
|
|
226
|
-
log.debug('Vibing orchestrator initialized with persisted workflows', undefined, 'vibing-orchestrator');
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
log.warn('Failed to load workflows from disk', error, 'vibing-orchestrator');
|
|
230
|
-
}
|
|
231
|
-
this.initialized = true;
|
|
232
|
-
}
|
|
233
|
-
// Normalize workflow shape so timeline/status writes don't throw
|
|
234
|
-
normalizeWorkflow(wf) {
|
|
235
|
-
const out = { ...wf };
|
|
236
|
-
if (!Array.isArray(out.phaseHistory))
|
|
237
|
-
out.phaseHistory = [];
|
|
238
|
-
if (!Array.isArray(out.timeline))
|
|
239
|
-
out.timeline = [];
|
|
240
|
-
if (!out.executionIds || typeof out.executionIds !== 'object')
|
|
241
|
-
out.executionIds = {};
|
|
242
|
-
const execPhases = ['implementing', 'validating', 'ai-reviewing', 'merging'];
|
|
243
|
-
for (const ph of execPhases) {
|
|
244
|
-
const arr = out.executionIds[ph];
|
|
245
|
-
if (!Array.isArray(arr))
|
|
246
|
-
out.executionIds[ph] = [];
|
|
247
|
-
}
|
|
248
|
-
if (!out.attempts || typeof out.attempts !== 'object')
|
|
249
|
-
out.attempts = {};
|
|
250
|
-
const metrics = (out.metrics ?? (out.metrics = { durationsMs: {} }));
|
|
251
|
-
if (!metrics.durationsMs)
|
|
252
|
-
metrics.durationsMs = {};
|
|
253
|
-
if (!metrics.lastPhaseStartedAt)
|
|
254
|
-
metrics.lastPhaseStartedAt = out.startTime;
|
|
255
|
-
if (!out.lastUpdatedAt)
|
|
256
|
-
out.lastUpdatedAt = out.startTime;
|
|
257
|
-
if (!out.status && out.phase === 'draft')
|
|
258
|
-
out.status = 'draft';
|
|
259
|
-
return out;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Execute a task once (no phase management). Optionally associates with a workflow.
|
|
263
|
-
*/
|
|
264
|
-
async executeTask(taskId, workflowId) {
|
|
265
|
-
const t = this.taskService.getTask(taskId);
|
|
266
|
-
if (!t)
|
|
267
|
-
throw new Error(`Task ${taskId} not found`);
|
|
268
|
-
// Wait for the executionCreated event for this task/workflow
|
|
269
|
-
const createdPromise = this.waitForExecutionCreated(taskId, workflowId, 15000);
|
|
270
|
-
// Kick off the agent execution (do not rely on returned id)
|
|
271
|
-
// Attach a catch handler to avoid unhandled promise rejection crashing the process
|
|
272
|
-
void this.agentService
|
|
273
|
-
.executeTask(taskId, workflowId)
|
|
274
|
-
.catch((err) => log.error('Agent execution failed to start (executeTask)', err, 'vibing-orchestrator'));
|
|
275
|
-
const executionId = await createdPromise;
|
|
276
|
-
return { executionId };
|
|
277
|
-
}
|
|
278
|
-
async stopExecution(executionId) {
|
|
279
|
-
await this.agentService.stopExecution(executionId);
|
|
280
|
-
}
|
|
281
|
-
getExecutionStatus(executionId) {
|
|
282
|
-
return this.agentService.getExecutionStatus(executionId);
|
|
283
|
-
}
|
|
284
|
-
async getPersistedExecutionLogs(executionId) {
|
|
285
|
-
return await this.agentService.getPersistedExecutionLogs(executionId);
|
|
286
|
-
}
|
|
287
|
-
getExecutionLogs(executionId) {
|
|
288
|
-
return this.agentService.getExecutionLogs(executionId);
|
|
289
|
-
}
|
|
290
|
-
getTaskExecutions(taskId) {
|
|
291
|
-
return this.agentService.getTaskExecutions(taskId);
|
|
292
|
-
}
|
|
293
|
-
getExecutionStats() {
|
|
294
|
-
return this.agentService.getExecutionStats();
|
|
295
|
-
}
|
|
296
|
-
async listAllExecutions() {
|
|
297
|
-
return await this.agentService.listAllExecutions();
|
|
298
|
-
}
|
|
299
|
-
async improveTaskContent(taskId, data, options) {
|
|
300
|
-
const task = this.taskService.getTask(taskId);
|
|
301
|
-
if (!task)
|
|
302
|
-
throw new Error(`Task ${taskId} not found`);
|
|
303
|
-
const res = await this.agentService.improveTaskContent(task, data, options?.executionId);
|
|
304
|
-
if (options?.workflowId) {
|
|
305
|
-
const wf = this.workflows.get(options.workflowId);
|
|
306
|
-
if (wf) {
|
|
307
|
-
wf.metadata = {
|
|
308
|
-
...wf.metadata,
|
|
309
|
-
lastTaskImprovement: { ...res, timestamp: new Date().toISOString() },
|
|
310
|
-
};
|
|
311
|
-
await this.saveWorkflow(wf);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return res;
|
|
315
|
-
}
|
|
316
|
-
async aiReviewCode(taskId, reviewContext, options) {
|
|
317
|
-
const res = await this.agentService.aiReviewCode(taskId, reviewContext, {
|
|
318
|
-
overrides: options?.overrides,
|
|
319
|
-
workflowId: options?.workflowId,
|
|
320
|
-
executionId: options?.executionId,
|
|
321
|
-
});
|
|
322
|
-
const wfId = options?.workflowId;
|
|
323
|
-
if (wfId) {
|
|
324
|
-
const wf = this.workflows.get(wfId);
|
|
325
|
-
if (wf) {
|
|
326
|
-
const timestamp = new Date().toISOString();
|
|
327
|
-
wf.metadata = {
|
|
328
|
-
...wf.metadata,
|
|
329
|
-
aiReviewResult: { ...res, timestamp },
|
|
330
|
-
};
|
|
331
|
-
wf.aiReviewResult = { ...res, timestamp };
|
|
332
|
-
// Record execution ID under ai-reviewing for UI/observability
|
|
333
|
-
if (res?.executionId) {
|
|
334
|
-
wf.executionIds = wf.executionIds || {};
|
|
335
|
-
const list = wf.executionIds['ai-reviewing'] || [];
|
|
336
|
-
wf.executionIds['ai-reviewing'] = [...list, res.executionId];
|
|
337
|
-
}
|
|
338
|
-
await this.saveWorkflow(wf);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return res;
|
|
342
|
-
}
|
|
343
|
-
async aiMerge(taskId, options) {
|
|
344
|
-
const overrides = options?.provider || options?.model
|
|
345
|
-
? { provider: options?.provider, model: options?.model }
|
|
346
|
-
: undefined;
|
|
347
|
-
return await this.agentService.aiMerge(taskId, {
|
|
348
|
-
baseBranch: options?.baseBranch,
|
|
349
|
-
workflowId: options?.workflowId,
|
|
350
|
-
executionId: options?.executionId,
|
|
351
|
-
overrides,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
// Worktree façade
|
|
355
|
-
async listWorktrees() {
|
|
356
|
-
return await this.worktreeService.listWorktrees();
|
|
357
|
-
}
|
|
358
|
-
async syncWorktreeState() {
|
|
359
|
-
await this.worktreeService.syncWorktreeState();
|
|
360
|
-
}
|
|
361
|
-
getWorktreeInfo(taskId) {
|
|
362
|
-
const wt = this.worktreeService.getWorktree(taskId);
|
|
363
|
-
if (!wt)
|
|
364
|
-
return null;
|
|
365
|
-
// Remove non-serializable references before exposing to callers
|
|
366
|
-
const { git, ...rest } = wt;
|
|
367
|
-
return rest;
|
|
368
|
-
}
|
|
369
|
-
async createWorktree(taskId) {
|
|
370
|
-
const task = this.taskService.getTask(taskId);
|
|
371
|
-
if (!task)
|
|
372
|
-
throw new Error(`Task ${taskId} not found`);
|
|
373
|
-
if (task.deleted_at)
|
|
374
|
-
throw new Error(`Task ${taskId} is deleted`);
|
|
375
|
-
const wt = await this.worktreeService.createWorktree(task);
|
|
376
|
-
const { git, ...rest } = wt;
|
|
377
|
-
return rest;
|
|
378
|
-
}
|
|
379
|
-
async deleteWorktree(taskId, force) {
|
|
380
|
-
await this.worktreeService.deleteWorktree(taskId, !!force);
|
|
381
|
-
}
|
|
382
|
-
async cleanupWorktree(params) {
|
|
383
|
-
await this.worktreeService.cleanupWorktree(params);
|
|
384
|
-
}
|
|
385
|
-
async createPullRequest(taskId, baseBranch = 'main') {
|
|
386
|
-
return await this.worktreeService.createPullRequest(taskId, baseBranch);
|
|
387
|
-
}
|
|
388
|
-
// Open in configured code editor (from settings.development.editor.command)
|
|
389
|
-
async openInEditor(worktreePath) {
|
|
390
|
-
try {
|
|
391
|
-
const settingsService = getSettingsService();
|
|
392
|
-
await settingsService.initialize();
|
|
393
|
-
const editor = settingsService.getSetting([
|
|
394
|
-
'development',
|
|
395
|
-
'editor',
|
|
396
|
-
]) || { command: 'code', args: [] };
|
|
397
|
-
const cmd = editor.command || 'code';
|
|
398
|
-
const baseArgs = Array.isArray(editor.args) ? editor.args : [];
|
|
399
|
-
spawn(cmd, [...baseArgs, worktreePath], {
|
|
400
|
-
stdio: 'ignore',
|
|
401
|
-
detached: true,
|
|
402
|
-
env: stripNextInjectedEnv(),
|
|
403
|
-
});
|
|
404
|
-
return { success: true };
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
// Fallback to cursor if settings service fails
|
|
408
|
-
spawn('cursor', [worktreePath], {
|
|
409
|
-
stdio: 'ignore',
|
|
410
|
-
detached: true,
|
|
411
|
-
env: stripNextInjectedEnv(),
|
|
412
|
-
});
|
|
413
|
-
return { success: true };
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async openInTerminal(worktreePath) {
|
|
417
|
-
// TODO: add options to resume the session
|
|
418
|
-
try {
|
|
419
|
-
const settingsService = getSettingsService();
|
|
420
|
-
await settingsService.initialize();
|
|
421
|
-
const terminal = settingsService.getSetting([
|
|
422
|
-
'development',
|
|
423
|
-
'terminal',
|
|
424
|
-
]) || {
|
|
425
|
-
command: '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal',
|
|
426
|
-
args: [],
|
|
427
|
-
};
|
|
428
|
-
const cmd = terminal.command || '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal';
|
|
429
|
-
const baseArgs = Array.isArray(terminal.args) ? terminal.args : [];
|
|
430
|
-
spawn(cmd, [...baseArgs, worktreePath], {
|
|
431
|
-
stdio: 'ignore',
|
|
432
|
-
detached: true,
|
|
433
|
-
env: stripNextInjectedEnv(),
|
|
434
|
-
});
|
|
435
|
-
return { success: true };
|
|
436
|
-
}
|
|
437
|
-
catch {
|
|
438
|
-
// Fallback to default terminal if settings service fails
|
|
439
|
-
spawn('/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal', [worktreePath], {
|
|
440
|
-
stdio: 'ignore',
|
|
441
|
-
detached: true,
|
|
442
|
-
env: stripNextInjectedEnv(),
|
|
443
|
-
});
|
|
444
|
-
return { success: true };
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
// Worktree status helpers used by UI gating flows
|
|
448
|
-
async checkWorktreeStatus(workflowId) {
|
|
449
|
-
const workflow = this.workflows.get(workflowId);
|
|
450
|
-
if (!workflow)
|
|
451
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
452
|
-
const worktree = this.getWorktreeInfo(workflow.taskId);
|
|
453
|
-
if (!worktree?.path)
|
|
454
|
-
throw new Error('No worktree found for this task');
|
|
455
|
-
const status = await this.worktreeService.getGitService().getGitStatus(worktree.path);
|
|
456
|
-
const files = [...status.modified, ...status.staged, ...status.not_added];
|
|
457
|
-
return { clean: files.length === 0, uncommittedFiles: files, worktreePath: worktree.path };
|
|
458
|
-
}
|
|
459
|
-
async approveAndContinueReview(workflowId) {
|
|
460
|
-
const status = await this.checkWorktreeStatus(workflowId);
|
|
461
|
-
if (!status.clean) {
|
|
462
|
-
throw new Error(`Uncommitted changes found. Please commit or stash the following files:\n${status.uncommittedFiles.join('\n')}`);
|
|
463
|
-
}
|
|
464
|
-
await this.approveWorkflow(workflowId);
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Mark workflow as completed by human decision.
|
|
468
|
-
* Does not perform merge/cleanup; simply finalizes the workflow and emits completion.
|
|
469
|
-
*/
|
|
470
|
-
async markWorkflowCompleted(workflowId) {
|
|
471
|
-
const workflow = this.workflows.get(workflowId);
|
|
472
|
-
if (!workflow)
|
|
473
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
474
|
-
// Record that completion was decided by human
|
|
475
|
-
workflow.metadata = {
|
|
476
|
-
...workflow.metadata,
|
|
477
|
-
humanCompleted: true,
|
|
478
|
-
};
|
|
479
|
-
workflow.endTime = new Date().toISOString();
|
|
480
|
-
await this.transitionToPhase(workflowId, 'completed');
|
|
481
|
-
this.emit('workflowCompleted', workflow);
|
|
482
|
-
log.info('Workflow marked completed by human', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
483
|
-
await this.saveWorkflow(workflow);
|
|
484
|
-
}
|
|
485
|
-
async ensureInitialized() {
|
|
486
|
-
if (!this.initialized) {
|
|
487
|
-
await this.initialize();
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Start AI-assisted workflow for a task
|
|
492
|
-
*/
|
|
493
|
-
async startWorkflow(taskId, customConfig) {
|
|
494
|
-
const task = this.taskService.getTask(taskId);
|
|
495
|
-
if (!task) {
|
|
496
|
-
throw new Error(`Task ${taskId} not found`);
|
|
497
|
-
}
|
|
498
|
-
const workflowId = generateId('workflow');
|
|
499
|
-
const defaultConfig = await getDefaultConfig();
|
|
500
|
-
// Ensure settings-derived defaults take precedence over constructor fallbacks
|
|
501
|
-
const finalConfig = { ...this.config, ...defaultConfig, ...customConfig };
|
|
502
|
-
const workflow = this.createNewWorkflow(workflowId, taskId, finalConfig);
|
|
503
|
-
this.workflows.set(workflowId, workflow);
|
|
504
|
-
// Persist the new workflow
|
|
505
|
-
await this.saveWorkflow(workflow);
|
|
506
|
-
this.emit('workflowStarted', workflow);
|
|
507
|
-
log.info('Starting workflow for task', { taskId, workflowId }, 'vibing-orchestrator');
|
|
508
|
-
await this.executeImplementation(workflowId);
|
|
509
|
-
return workflowId;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Pause a workflow execution
|
|
513
|
-
*/
|
|
514
|
-
async pauseWorkflow(workflowId) {
|
|
515
|
-
const workflow = this.workflows.get(workflowId);
|
|
516
|
-
if (!workflow) {
|
|
517
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
518
|
-
}
|
|
519
|
-
// Stop current execution if running
|
|
520
|
-
// Try to stop the most recent running execution for this task
|
|
521
|
-
const executions = this.agentService.getTaskExecutions(workflow.taskId) || [];
|
|
522
|
-
const running = executions
|
|
523
|
-
.filter((e) => e.status === 'running')
|
|
524
|
-
.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())[0];
|
|
525
|
-
const latest = running ||
|
|
526
|
-
executions.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())[0];
|
|
527
|
-
if (latest?.id) {
|
|
528
|
-
try {
|
|
529
|
-
await this.agentService.stopExecution(latest.id);
|
|
530
|
-
}
|
|
531
|
-
catch (error) {
|
|
532
|
-
log.warn('Failed to stop execution', error, 'vibing-orchestrator');
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
workflow.phase = 'paused';
|
|
536
|
-
workflow.error = 'Paused by user';
|
|
537
|
-
workflow.status = 'paused';
|
|
538
|
-
// Persist the workflow state
|
|
539
|
-
await this.saveWorkflow(workflow);
|
|
540
|
-
this.emit('workflowPaused', workflow);
|
|
541
|
-
log.info('Paused workflow', { workflowId }, 'vibing-orchestrator');
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Reset a workflow by deleting its record and optionally its worktree.
|
|
545
|
-
*/
|
|
546
|
-
async resetWorkflow(workflowId, options) {
|
|
547
|
-
const wf = this.workflows.get(workflowId);
|
|
548
|
-
if (!wf)
|
|
549
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
550
|
-
const taskId = wf.taskId;
|
|
551
|
-
if (options?.deleteWorktree) {
|
|
552
|
-
try {
|
|
553
|
-
await this.deleteWorktree(wf.taskId, !!options.force);
|
|
554
|
-
}
|
|
555
|
-
catch (e) {
|
|
556
|
-
log.warn('Failed to delete worktree during reset', e, 'vibing-orchestrator');
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
// Remove from memory and persistence
|
|
560
|
-
this.workflows.delete(workflowId);
|
|
561
|
-
try {
|
|
562
|
-
await this.workflowDB.delete(workflowId);
|
|
563
|
-
}
|
|
564
|
-
catch (e) {
|
|
565
|
-
log.warn('Failed to delete workflow persistence', e, 'vibing-orchestrator');
|
|
566
|
-
}
|
|
567
|
-
if (taskId) {
|
|
568
|
-
try {
|
|
569
|
-
const removed = await this.removeWorkflowsByTask(taskId);
|
|
570
|
-
if (removed > 0) {
|
|
571
|
-
log.info('Deleted additional workflows for task during reset', { taskId, removed }, 'vibing-orchestrator');
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
catch (e) {
|
|
575
|
-
log.warn('Failed to delete all workflows for task on reset', e, 'vibing-orchestrator');
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
this.emit('workflowDeleted', { workflowId, taskId: wf.taskId });
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Approve workflow changes and proceed to merge
|
|
582
|
-
*/
|
|
583
|
-
async approveWorkflow(workflowId) {
|
|
584
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
585
|
-
this.assertPhase(workflow, 'awaiting-review', 'approve');
|
|
586
|
-
// Close the awaiting-review timeline item if present
|
|
587
|
-
try {
|
|
588
|
-
const tl = (workflow.timeline || []);
|
|
589
|
-
for (let i = tl.length - 1; i >= 0; i--) {
|
|
590
|
-
const t = tl[i];
|
|
591
|
-
if (t.phase === 'awaiting-review' && !t.placeholder && !t.endTime) {
|
|
592
|
-
t.endTime = new Date().toISOString();
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
597
|
-
await this.saveWorkflow(workflow);
|
|
598
|
-
}
|
|
599
|
-
catch (err) {
|
|
600
|
-
log.debug('Failed to finalize awaiting-review timeline item', err, 'vibing-orchestrator');
|
|
601
|
-
}
|
|
602
|
-
workflow.status = 'approved';
|
|
603
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
604
|
-
await this.transitionToPhase(workflowId, 'approved');
|
|
605
|
-
await this.saveWorkflow(workflow);
|
|
606
|
-
if (workflow.metadata.stepByStepMode) {
|
|
607
|
-
log.info('Manual step mode – approved; waiting for Continue to run merge', { workflowId }, 'vibing-orchestrator');
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
await this.executeMerge(workflowId);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Reject workflow and request changes
|
|
615
|
-
*/
|
|
616
|
-
async rejectWorkflow(workflowId, feedback) {
|
|
617
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
618
|
-
this.assertPhase(workflow, 'awaiting-review', 'reject');
|
|
619
|
-
// Close the awaiting-review timeline item if present
|
|
620
|
-
try {
|
|
621
|
-
const tl = (workflow.timeline || []);
|
|
622
|
-
for (let i = tl.length - 1; i >= 0; i--) {
|
|
623
|
-
const t = tl[i];
|
|
624
|
-
if (t.phase === 'awaiting-review' && !t.placeholder && !t.endTime) {
|
|
625
|
-
t.endTime = new Date().toISOString();
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
630
|
-
await this.saveWorkflow(workflow);
|
|
631
|
-
}
|
|
632
|
-
catch (err) {
|
|
633
|
-
log.debug('Failed to finalize awaiting-review timeline item', err, 'vibing-orchestrator');
|
|
634
|
-
}
|
|
635
|
-
// Return to implementation phase with feedback
|
|
636
|
-
await this.transitionToPhase(workflowId, 'implementing');
|
|
637
|
-
await this.executeImplementation(workflowId, feedback);
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Get workflow status
|
|
641
|
-
*/
|
|
642
|
-
getWorkflow(workflowId) {
|
|
643
|
-
const wf = this.workflows.get(workflowId) || null;
|
|
644
|
-
if (wf) {
|
|
645
|
-
wf.timeline = this.computeVisibleTimeline(wf);
|
|
646
|
-
}
|
|
647
|
-
return wf;
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Get all workflows for a task
|
|
651
|
-
*/
|
|
652
|
-
getTaskWorkflows(taskId) {
|
|
653
|
-
return Array.from(this.workflows.values())
|
|
654
|
-
.filter((w) => w.taskId === taskId)
|
|
655
|
-
.map((wf) => {
|
|
656
|
-
wf.timeline = this.computeVisibleTimeline(wf);
|
|
657
|
-
return wf;
|
|
658
|
-
})
|
|
659
|
-
.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Get latest workflow for a task, preferring most recently updated.
|
|
663
|
-
* Falls back to newest startTime when lastUpdatedAt is unavailable.
|
|
664
|
-
*/
|
|
665
|
-
getLatestWorkflowByTask(taskId) {
|
|
666
|
-
const list = Array.from(this.workflows.values()).filter((w) => w.taskId === taskId);
|
|
667
|
-
if (list.length === 0)
|
|
668
|
-
return null;
|
|
669
|
-
const sorted = list.sort((a, b) => {
|
|
670
|
-
const aTs = new Date(a.lastUpdatedAt || a.startTime || 0).getTime();
|
|
671
|
-
const bTs = new Date(b.lastUpdatedAt || b.startTime || 0).getTime();
|
|
672
|
-
return bTs - aTs;
|
|
673
|
-
});
|
|
674
|
-
const latest = sorted[0];
|
|
675
|
-
latest.timeline = this.computeVisibleTimeline(latest);
|
|
676
|
-
return latest;
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Transition workflow to new phase
|
|
680
|
-
*/
|
|
681
|
-
async transitionToPhase(workflowId, newPhase) {
|
|
682
|
-
const workflow = this.workflows.get(workflowId);
|
|
683
|
-
if (!workflow)
|
|
684
|
-
return;
|
|
685
|
-
const oldPhase = workflow.phase;
|
|
686
|
-
workflow.lastPhase = oldPhase;
|
|
687
|
-
workflow.phase = newPhase;
|
|
688
|
-
const history = workflow.phaseHistory || [];
|
|
689
|
-
const now = new Date().toISOString();
|
|
690
|
-
history.push({ from: oldPhase, to: newPhase, at: now });
|
|
691
|
-
workflow.phaseHistory = history;
|
|
692
|
-
// Metrics accumulation
|
|
693
|
-
const metrics = (workflow.metrics = workflow.metrics || { durationsMs: {} });
|
|
694
|
-
const startTs = metrics.lastPhaseStartedAt
|
|
695
|
-
? new Date(metrics.lastPhaseStartedAt).getTime()
|
|
696
|
-
: new Date(workflow.startTime).getTime();
|
|
697
|
-
const nowTs = new Date(now).getTime();
|
|
698
|
-
const delta = Math.max(0, nowTs - startTs);
|
|
699
|
-
const prev = metrics.durationsMs?.[oldPhase] || 0;
|
|
700
|
-
metrics.durationsMs = { ...metrics.durationsMs, [oldPhase]: prev + delta };
|
|
701
|
-
metrics.lastPhaseStartedAt = now;
|
|
702
|
-
if (newPhase === 'completed') {
|
|
703
|
-
metrics.totalDurationMs = Object.values(metrics.durationsMs || {}).reduce((a, b) => a + (b || 0), 0);
|
|
704
|
-
}
|
|
705
|
-
workflow.lastUpdatedAt = now;
|
|
706
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
707
|
-
log.info('Workflow phase transition', { workflowId, oldPhase, newPhase }, 'vibing-orchestrator');
|
|
708
|
-
await this.saveWorkflow(workflow);
|
|
709
|
-
this.emit('workflowPhaseChanged', { workflowId, oldPhase, newPhase, workflow });
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Save workflow to persistence layer
|
|
713
|
-
*/
|
|
714
|
-
async saveWorkflow(workflow) {
|
|
715
|
-
try {
|
|
716
|
-
await this.workflowDB.set(workflow.id, serializeVibe(workflow));
|
|
717
|
-
}
|
|
718
|
-
catch (error) {
|
|
719
|
-
log.warn('Failed to persist workflow', error, 'vibing-orchestrator');
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Execute implementation phase with AI coding
|
|
724
|
-
*/
|
|
725
|
-
async executeImplementation(workflowId, feedback, providerOverride) {
|
|
726
|
-
const workflow = this.workflows.get(workflowId);
|
|
727
|
-
if (!workflow)
|
|
728
|
-
return;
|
|
729
|
-
await this.transitionToPhase(workflowId, 'implementing');
|
|
730
|
-
try {
|
|
731
|
-
log.info('Implementing task', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
732
|
-
// Start task execution with enhanced prompts and workflow context
|
|
733
|
-
// Prepare rerun context if any
|
|
734
|
-
const previousExecId = (workflow.executionIds?.['implementing'] || []).slice(-1)[0];
|
|
735
|
-
const attempt = (workflow.attempts?.implementing || 0) + 1;
|
|
736
|
-
// Build rerun context automatically when available (no need to plumb reason externally)
|
|
737
|
-
const autoReason = feedback ||
|
|
738
|
-
workflow.failureContext?.error ||
|
|
739
|
-
(workflow.failureContext?.atPhase
|
|
740
|
-
? `Retry after ${workflow.failureContext.atPhase} failure`
|
|
741
|
-
: undefined);
|
|
742
|
-
const aiReviewSource = workflow.aiReviewResult || workflow.metadata?.aiReviewResult;
|
|
743
|
-
const aiReviewContext = aiReviewSource
|
|
744
|
-
? {
|
|
745
|
-
summary: aiReviewSource.reviewSummary,
|
|
746
|
-
recommendations: aiReviewSource.recommendations,
|
|
747
|
-
score: aiReviewSource.qualityScore,
|
|
748
|
-
}
|
|
749
|
-
: undefined;
|
|
750
|
-
// Prepare to capture executionId from event, then start execution
|
|
751
|
-
const createdPromise = this.waitForExecutionCreated(workflow.taskId, workflowId, 20000);
|
|
752
|
-
// Fire-and-forget; attach catch to prevent unhandled rejection from crashing process
|
|
753
|
-
void this.agentService
|
|
754
|
-
.executeTask(workflow.taskId, workflowId, {
|
|
755
|
-
rerunContext: autoReason
|
|
756
|
-
? {
|
|
757
|
-
reason: autoReason,
|
|
758
|
-
previousExecutionId: previousExecId,
|
|
759
|
-
failurePhase: workflow.failureContext?.atPhase,
|
|
760
|
-
qualityResults: workflow.qualityResults,
|
|
761
|
-
aiReview: aiReviewContext,
|
|
762
|
-
attempt,
|
|
763
|
-
}
|
|
764
|
-
: undefined,
|
|
765
|
-
providerOverride: providerOverride || workflow.metadata?.aiRoutingOverrides?.execute_task,
|
|
766
|
-
workflowConfig: workflow.metadata,
|
|
767
|
-
})
|
|
768
|
-
.catch((err) => log.error('Agent execution failed to start (implementation phase)', err, 'vibing-orchestrator'));
|
|
769
|
-
const executionId = await createdPromise;
|
|
770
|
-
// Maintain placeholders and wait for execution to complete
|
|
771
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
772
|
-
await this.saveWorkflow(workflow);
|
|
773
|
-
// Wait for execution to complete
|
|
774
|
-
await this.waitForExecution(executionId);
|
|
775
|
-
const execution = this.agentService.getExecutionStatus(executionId);
|
|
776
|
-
if (!execution || execution.status === 'failed') {
|
|
777
|
-
throw new Error(`Implementation failed: ${execution?.error || 'Unknown error'}`);
|
|
778
|
-
}
|
|
779
|
-
// Timeline: complete implementation attempt
|
|
780
|
-
const tImpl = workflow.timeline.find((t) => t.executionId === executionId);
|
|
781
|
-
if (tImpl) {
|
|
782
|
-
tImpl.endTime = execution.endTime || new Date().toISOString();
|
|
783
|
-
tImpl.usage = execution.usage;
|
|
784
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
785
|
-
await this.saveWorkflow(workflow);
|
|
786
|
-
}
|
|
787
|
-
log.info('Implementation completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
788
|
-
// Update checkpoint status
|
|
789
|
-
workflow.status = 'implemented';
|
|
790
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
791
|
-
await this.saveWorkflow(workflow);
|
|
792
|
-
await this.transitionToPhase(workflowId, 'implemented');
|
|
793
|
-
if (workflow.metadata.stepByStepMode) {
|
|
794
|
-
log.info('Manual step mode – implemented; waiting for Continue to run validation', { workflowId }, 'vibing-orchestrator');
|
|
795
|
-
}
|
|
796
|
-
else {
|
|
797
|
-
await this.executeValidation(workflowId);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
catch (error) {
|
|
801
|
-
await this.handlePhaseFailure(workflowId, 'implementing', error);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Public: re-run implementation phase on demand
|
|
806
|
-
* Forces phase to 'implementing' and executes implementation again.
|
|
807
|
-
*/
|
|
808
|
-
async rerunImplementation(workflowId, feedback, providerOverride) {
|
|
809
|
-
const workflow = this.workflows.get(workflowId);
|
|
810
|
-
if (!workflow)
|
|
811
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
812
|
-
await this.transitionToPhase(workflowId, 'implementing');
|
|
813
|
-
await this.executeImplementation(workflowId, feedback, providerOverride);
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Get previous phase, preferring the phase where failure occurred
|
|
817
|
-
*/
|
|
818
|
-
getPreviousPhase(workflowId) {
|
|
819
|
-
const wf = this.workflows.get(workflowId);
|
|
820
|
-
if (!wf)
|
|
821
|
-
return null;
|
|
822
|
-
return wf.failureContext?.atPhase || wf.lastPhase || null;
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Rerun the phase that led to failure (or lastPhase if available)
|
|
826
|
-
*/
|
|
827
|
-
async rerunPreviousPhase(workflowId) {
|
|
828
|
-
const phase = this.getPreviousPhase(workflowId);
|
|
829
|
-
if (!phase)
|
|
830
|
-
throw new Error('No previous phase recorded.');
|
|
831
|
-
await this.rerunPhase(workflowId, phase);
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Rerun a specific phase programmatically
|
|
835
|
-
*/
|
|
836
|
-
async rerunPhase(workflowId, phase) {
|
|
837
|
-
// Before rerun, reset timeline and results from the selected phase onward
|
|
838
|
-
await this.resetFromPhase(workflowId, phase);
|
|
839
|
-
switch (phase) {
|
|
840
|
-
case 'implementing':
|
|
841
|
-
await this.transitionToPhase(workflowId, 'implementing');
|
|
842
|
-
await this.executeImplementation(workflowId);
|
|
843
|
-
return;
|
|
844
|
-
case 'validating':
|
|
845
|
-
await this.executeValidation(workflowId);
|
|
846
|
-
return;
|
|
847
|
-
case 'ai-reviewing':
|
|
848
|
-
await this.executeAiReviewPhase(workflowId);
|
|
849
|
-
return;
|
|
850
|
-
case 'awaiting-review':
|
|
851
|
-
await this.executeAwaitingReview(workflowId);
|
|
852
|
-
return;
|
|
853
|
-
case 'merging':
|
|
854
|
-
await this.executeMerge(workflowId);
|
|
855
|
-
return;
|
|
856
|
-
case 'approved':
|
|
857
|
-
// Failure after approval typically means merge failed; try merging again
|
|
858
|
-
await this.executeMerge(workflowId);
|
|
859
|
-
return;
|
|
860
|
-
case 'cleaning':
|
|
861
|
-
await this.executeCleanup(workflowId);
|
|
862
|
-
return;
|
|
863
|
-
default:
|
|
864
|
-
// For other phases, restart implementation as a safe default
|
|
865
|
-
await this.executeImplementation(workflowId);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Reset workflow state from a given phase onward (inclusive).
|
|
871
|
-
* Clears timeline entries, execution IDs, attempts, and phase results for later phases
|
|
872
|
-
* so that a rerun behaves like a fresh run from that step.
|
|
873
|
-
*/
|
|
874
|
-
async resetFromPhase(workflowId, from) {
|
|
875
|
-
const workflow = this.workflows.get(workflowId);
|
|
876
|
-
if (!workflow)
|
|
877
|
-
return;
|
|
878
|
-
const ordered = [
|
|
879
|
-
'draft',
|
|
880
|
-
'implementing',
|
|
881
|
-
'validating',
|
|
882
|
-
'ai-reviewing',
|
|
883
|
-
'awaiting-review',
|
|
884
|
-
'approved',
|
|
885
|
-
'merging',
|
|
886
|
-
'merged',
|
|
887
|
-
'cleaning',
|
|
888
|
-
'completed',
|
|
889
|
-
];
|
|
890
|
-
const idx = ordered.indexOf(from);
|
|
891
|
-
if (idx < 0)
|
|
892
|
-
return;
|
|
893
|
-
const toReset = ordered.slice(idx); // inclusive of `from`
|
|
894
|
-
// Clear execution IDs and attempts for affected phases
|
|
895
|
-
workflow.executionIds = workflow.executionIds || {};
|
|
896
|
-
workflow.attempts = workflow.attempts || {};
|
|
897
|
-
for (const ph of toReset) {
|
|
898
|
-
if (workflow.executionIds[ph])
|
|
899
|
-
workflow.executionIds[ph] = [];
|
|
900
|
-
if (workflow.attempts[ph] !== undefined)
|
|
901
|
-
delete workflow.attempts[ph];
|
|
902
|
-
}
|
|
903
|
-
// Trim timeline items from affected phases
|
|
904
|
-
if (Array.isArray(workflow.timeline)) {
|
|
905
|
-
workflow.timeline = workflow.timeline.filter((t) => ordered.indexOf(t.phase) < idx);
|
|
906
|
-
}
|
|
907
|
-
// Trim phase history entries from affected phases so UI does not reconstruct later steps
|
|
908
|
-
if (Array.isArray(workflow.phaseHistory)) {
|
|
909
|
-
workflow.phaseHistory = workflow.phaseHistory.filter((h) => ordered.indexOf(h.to) < idx);
|
|
910
|
-
// Update lastPhase to the last remaining history entry (or 'draft')
|
|
911
|
-
const last = workflow.phaseHistory[workflow.phaseHistory.length - 1];
|
|
912
|
-
workflow.lastPhase = (last ? last.to : 'draft');
|
|
913
|
-
}
|
|
914
|
-
// Clear phase-specific results when rerunning from or before them
|
|
915
|
-
if (idx <= ordered.indexOf('validating')) {
|
|
916
|
-
workflow.qualityResults = undefined;
|
|
917
|
-
}
|
|
918
|
-
if (idx <= ordered.indexOf('ai-reviewing')) {
|
|
919
|
-
if (workflow.metadata) {
|
|
920
|
-
const { aiReviewResult, ...rest } = workflow.metadata || {};
|
|
921
|
-
workflow.metadata = { ...rest };
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
// Clear any failure/error context
|
|
925
|
-
workflow.error = undefined;
|
|
926
|
-
workflow.failureContext = undefined;
|
|
927
|
-
// Persist reset before kicking off rerun
|
|
928
|
-
await this.saveWorkflow(workflow);
|
|
929
|
-
// Notify listeners that the workflow snapshot changed (phase change will follow)
|
|
930
|
-
this.emit('workflowUpdated', workflow);
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Perform AI code review of implemented changes
|
|
934
|
-
*/
|
|
935
|
-
async performAICodeReview(workflowId, providedExecutionId) {
|
|
936
|
-
const workflow = this.workflows.get(workflowId);
|
|
937
|
-
if (!workflow)
|
|
938
|
-
return { passed: false };
|
|
939
|
-
try {
|
|
940
|
-
log.info('Running AI code review', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
941
|
-
// Get worktree info for the task
|
|
942
|
-
const worktree = this.agentService.getWorktreeInfo(workflow.taskId);
|
|
943
|
-
if (!worktree) {
|
|
944
|
-
log.warn('No worktree found for AI code review, skipping', undefined, 'vibing-orchestrator');
|
|
945
|
-
return { passed: true, executionId: providedExecutionId };
|
|
946
|
-
}
|
|
947
|
-
// Execute AI code review via agent service
|
|
948
|
-
const review = await this.agentService.aiReviewCode(workflow.taskId, 'Automated AI code review triggered by workflow orchestrator', {
|
|
949
|
-
overrides: workflow.metadata?.aiRoutingOverrides?.ai_codereview,
|
|
950
|
-
executionId: providedExecutionId,
|
|
951
|
-
workflowId,
|
|
952
|
-
});
|
|
953
|
-
// Persist review result into workflow metadata
|
|
954
|
-
const timestamp = new Date().toISOString();
|
|
955
|
-
workflow.metadata = {
|
|
956
|
-
...workflow.metadata,
|
|
957
|
-
aiReviewResult: { ...review, timestamp },
|
|
958
|
-
};
|
|
959
|
-
workflow.aiReviewResult = { ...review, timestamp };
|
|
960
|
-
// Track execution ID under ai-reviewing for observability
|
|
961
|
-
const executionId = providedExecutionId ?? review?.executionId;
|
|
962
|
-
if (executionId) {
|
|
963
|
-
workflow.executionIds = workflow.executionIds || {};
|
|
964
|
-
const list = workflow.executionIds['ai-reviewing'] || [];
|
|
965
|
-
if (!list.includes(executionId)) {
|
|
966
|
-
workflow.executionIds['ai-reviewing'] = [...list, executionId];
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
await this.saveWorkflow(workflow);
|
|
970
|
-
// Gate by minimum score from settings
|
|
971
|
-
const minScore = await this.getAIReviewThreshold();
|
|
972
|
-
if (review.qualityScore < minScore) {
|
|
973
|
-
const msg = `AI code review score too low (${review.qualityScore} < ${minScore})`;
|
|
974
|
-
log.warn('AI code review score too low', { score: review.qualityScore, minScore }, 'vibing-orchestrator');
|
|
975
|
-
workflow.error = msg;
|
|
976
|
-
return { passed: false, executionId };
|
|
977
|
-
}
|
|
978
|
-
log.info('AI code review passed', { score: review.qualityScore }, 'vibing-orchestrator');
|
|
979
|
-
workflow.error = undefined;
|
|
980
|
-
return { passed: true, executionId };
|
|
981
|
-
}
|
|
982
|
-
catch (error) {
|
|
983
|
-
log.warn('AI code review failed', error, 'vibing-orchestrator');
|
|
984
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
985
|
-
const workflowRef = this.workflows.get(workflowId);
|
|
986
|
-
if (workflowRef) {
|
|
987
|
-
workflowRef.error = `AI code review failed: ${msg}`;
|
|
988
|
-
}
|
|
989
|
-
return { passed: false, executionId: providedExecutionId };
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Execute AI review as a dedicated phase
|
|
994
|
-
*/
|
|
995
|
-
async executeAiReviewPhase(workflowId) {
|
|
996
|
-
const workflow = this.workflows.get(workflowId);
|
|
997
|
-
if (!workflow)
|
|
998
|
-
return;
|
|
999
|
-
await this.transitionToPhase(workflowId, 'ai-reviewing');
|
|
1000
|
-
try {
|
|
1001
|
-
const attemptList = workflow.executionIds?.['ai-reviewing'] || [];
|
|
1002
|
-
const attempt = attemptList.length + 1;
|
|
1003
|
-
const execId = generateId('review');
|
|
1004
|
-
workflow.executionIds = workflow.executionIds || {};
|
|
1005
|
-
const aiList = workflow.executionIds['ai-reviewing'] || [];
|
|
1006
|
-
workflow.executionIds['ai-reviewing'] = [...aiList, execId];
|
|
1007
|
-
workflow.timeline = this.buildTimeline(workflow, {
|
|
1008
|
-
id: execId,
|
|
1009
|
-
label: attempt > 1 ? `AI Review – Retry #${attempt}` : 'AI Review',
|
|
1010
|
-
phase: 'ai-reviewing',
|
|
1011
|
-
attempt,
|
|
1012
|
-
executionId: execId,
|
|
1013
|
-
startTime: new Date().toISOString(),
|
|
1014
|
-
});
|
|
1015
|
-
await this.saveWorkflow(workflow);
|
|
1016
|
-
this.emit('workflowExecutionStarted', {
|
|
1017
|
-
workflowId,
|
|
1018
|
-
executionId: execId,
|
|
1019
|
-
phase: 'ai-reviewing',
|
|
1020
|
-
workflow,
|
|
1021
|
-
});
|
|
1022
|
-
const { passed } = await this.performAICodeReview(workflowId, execId);
|
|
1023
|
-
const latest = this.workflows.get(workflowId);
|
|
1024
|
-
if (latest) {
|
|
1025
|
-
const timeline = (latest.timeline || []);
|
|
1026
|
-
const item = timeline.find((t) => t.executionId === execId);
|
|
1027
|
-
if (item) {
|
|
1028
|
-
item.endTime = new Date().toISOString();
|
|
1029
|
-
if (!passed) {
|
|
1030
|
-
item.reason = latest.error || 'AI review did not pass the quality threshold';
|
|
1031
|
-
}
|
|
1032
|
-
latest.timeline = this.computeVisibleTimeline(latest);
|
|
1033
|
-
await this.saveWorkflow(latest);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
if (!passed) {
|
|
1037
|
-
const reason = workflow.error || 'AI review did not pass the quality threshold';
|
|
1038
|
-
const retried = await this.maybeRetryImplementation(workflowId, reason, 'ai-reviewing');
|
|
1039
|
-
if (!retried) {
|
|
1040
|
-
await this.handlePhaseFailure(workflowId, 'ai-reviewing', workflow.error);
|
|
1041
|
-
}
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
// AI review passed → set checkpoint
|
|
1045
|
-
workflow.status = 'ai-reviewed';
|
|
1046
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1047
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
1048
|
-
await this.saveWorkflow(workflow);
|
|
1049
|
-
workflow.status = 'awaiting-review';
|
|
1050
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1051
|
-
await this.saveWorkflow(workflow);
|
|
1052
|
-
await this.executeAwaitingReview(workflowId);
|
|
1053
|
-
const wf = this.workflows.get(workflowId);
|
|
1054
|
-
if (wf?.metadata.stepByStepMode) {
|
|
1055
|
-
log.info('Manual step mode – AI review passed; waiting for Continue', { workflowId }, 'vibing-orchestrator');
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
catch (error) {
|
|
1059
|
-
await this.handlePhaseFailure(workflowId, 'ai-reviewing', error);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
async executeAwaitingReview(workflowId) {
|
|
1063
|
-
const workflow = this.workflows.get(workflowId);
|
|
1064
|
-
if (!workflow)
|
|
1065
|
-
return;
|
|
1066
|
-
await this.transitionToPhase(workflowId, 'awaiting-review');
|
|
1067
|
-
const hasReal = Array.isArray(workflow.timeline)
|
|
1068
|
-
? workflow.timeline.some((t) => t.phase === 'awaiting-review' && !t.placeholder)
|
|
1069
|
-
: false;
|
|
1070
|
-
if (!hasReal) {
|
|
1071
|
-
workflow.timeline = this.buildTimeline(workflow, {
|
|
1072
|
-
id: `awaiting-review:${new Date().toISOString()}`,
|
|
1073
|
-
label: PHASE_LABELS['awaiting-review'],
|
|
1074
|
-
phase: 'awaiting-review',
|
|
1075
|
-
startTime: new Date().toISOString(),
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
1079
|
-
await this.saveWorkflow(workflow);
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Execute validation phase with quality checks
|
|
1083
|
-
*/
|
|
1084
|
-
async executeValidation(workflowId) {
|
|
1085
|
-
const workflow = this.workflows.get(workflowId);
|
|
1086
|
-
if (!workflow)
|
|
1087
|
-
return;
|
|
1088
|
-
await this.transitionToPhase(workflowId, 'validating');
|
|
1089
|
-
try {
|
|
1090
|
-
if (!workflow.metadata.autoQualityChecks) {
|
|
1091
|
-
// TODO implement later
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
log.info('Running quality checks', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1095
|
-
// Get worktree path for quality checks
|
|
1096
|
-
const worktree = this.agentService.getWorktreeInfo(workflow.taskId);
|
|
1097
|
-
const workingDirectory = worktree?.path || process.cwd();
|
|
1098
|
-
// Start a generic execution for validation to unify observability
|
|
1099
|
-
const execId = await this.agentService.startGenericExecution(workflow.taskId, {
|
|
1100
|
-
workflowId,
|
|
1101
|
-
workingDirectory,
|
|
1102
|
-
});
|
|
1103
|
-
const qualityOverride = workflow.metadata?.aiRoutingOverrides?.quality_checks;
|
|
1104
|
-
try {
|
|
1105
|
-
const resolved = await this.agentService.previewProviderForOperation('quality_checks', qualityOverride);
|
|
1106
|
-
const providerLabel = resolved.model
|
|
1107
|
-
? `${resolved.provider} (model: ${resolved.model})`
|
|
1108
|
-
: resolved.provider;
|
|
1109
|
-
await this.agentService.logGenericExecution(execId, `[validation] Using provider: ${providerLabel}`);
|
|
1110
|
-
}
|
|
1111
|
-
catch (err) {
|
|
1112
|
-
log.warn('Failed to resolve quality-check provider', err, 'vibing-orchestrator');
|
|
1113
|
-
}
|
|
1114
|
-
workflow.executionIds = workflow.executionIds || {};
|
|
1115
|
-
const valList = workflow.executionIds['validating'] || [];
|
|
1116
|
-
workflow.executionIds['validating'] = [...valList, execId];
|
|
1117
|
-
workflow.timeline = this.buildTimeline(workflow, {
|
|
1118
|
-
id: `validation:${new Date().toISOString()}`,
|
|
1119
|
-
label: 'Validation',
|
|
1120
|
-
phase: 'validating',
|
|
1121
|
-
startTime: new Date().toISOString(),
|
|
1122
|
-
executionId: execId,
|
|
1123
|
-
});
|
|
1124
|
-
await this.saveWorkflow(workflow);
|
|
1125
|
-
// Notify listeners that a validation execution has started
|
|
1126
|
-
this.emit('workflowExecutionStarted', {
|
|
1127
|
-
workflowId,
|
|
1128
|
-
executionId: execId,
|
|
1129
|
-
phase: 'validating',
|
|
1130
|
-
workflow,
|
|
1131
|
-
});
|
|
1132
|
-
// Capture quality checks used for this workflow (from settings if not already set)
|
|
1133
|
-
try {
|
|
1134
|
-
if (!workflow.metadata.qualityChecks || workflow.metadata.qualityChecks.length === 0) {
|
|
1135
|
-
const { getSettingsService } = await import('../settings-service.js');
|
|
1136
|
-
const settingsService = getSettingsService();
|
|
1137
|
-
await settingsService.initialize();
|
|
1138
|
-
const settings = settingsService.getSettings();
|
|
1139
|
-
if (settings.quality?.checks?.length) {
|
|
1140
|
-
workflow.metadata.qualityChecks = settings.quality.checks;
|
|
1141
|
-
await this.saveWorkflow(workflow);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
catch (e) {
|
|
1146
|
-
log.warn('Failed to get quality checks from settings', e, 'vibing-orchestrator');
|
|
1147
|
-
}
|
|
1148
|
-
// Run quality pipeline with streaming logs
|
|
1149
|
-
const qualityResults = await this.qualityPipeline.runChecksStreaming(workingDirectory, async (line) => {
|
|
1150
|
-
try {
|
|
1151
|
-
await this.agentService.logGenericExecution(execId, String(line));
|
|
1152
|
-
}
|
|
1153
|
-
catch (err) {
|
|
1154
|
-
log.debug('Failed to stream validation log line', err, 'vibing-orchestrator');
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
workflow.qualityResults = qualityResults;
|
|
1158
|
-
// Timeline: end validation
|
|
1159
|
-
const timeline = workflow.timeline;
|
|
1160
|
-
for (let i = timeline.length - 1; i >= 0; i--) {
|
|
1161
|
-
const t = timeline[i];
|
|
1162
|
-
if (t.phase === 'validating' && !t.endTime) {
|
|
1163
|
-
t.endTime = new Date().toISOString();
|
|
1164
|
-
break;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
// Mark execution complete (completed even if checks failed; workflow handles failure)
|
|
1168
|
-
await this.agentService.completeGenericExecution(execId, 'completed');
|
|
1169
|
-
workflow.timeline = this.computeVisibleTimeline(workflow);
|
|
1170
|
-
await this.saveWorkflow(workflow);
|
|
1171
|
-
if (!qualityResults.overall) {
|
|
1172
|
-
log.info('Quality checks failed', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1173
|
-
workflow.error = 'Quality checks failed';
|
|
1174
|
-
const reason = 'Quality checks failed. Please address the reported issues.';
|
|
1175
|
-
const retried = await this.maybeRetryImplementation(workflowId, reason, 'validating');
|
|
1176
|
-
if (!retried) {
|
|
1177
|
-
await this.handlePhaseFailure(workflowId, 'validating', workflow.error);
|
|
1178
|
-
}
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
log.info('Quality checks passed', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1182
|
-
// Update checkpoint status
|
|
1183
|
-
await this.transitionToPhase(workflowId, 'validated');
|
|
1184
|
-
workflow.status = 'validated';
|
|
1185
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1186
|
-
await this.saveWorkflow(workflow);
|
|
1187
|
-
if (workflow.metadata.aiCodeReview) {
|
|
1188
|
-
if (workflow.metadata.stepByStepMode) {
|
|
1189
|
-
log.info('Manual step mode – waiting for Continue to run AI review', { workflowId }, 'vibing-orchestrator');
|
|
1190
|
-
}
|
|
1191
|
-
else {
|
|
1192
|
-
await this.executeAiReviewPhase(workflowId);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
catch (error) {
|
|
1197
|
-
await this.handlePhaseFailure(workflowId, 'validating', error);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Public: run validation phase on demand
|
|
1202
|
-
*/
|
|
1203
|
-
async runValidation(workflowId) {
|
|
1204
|
-
const workflow = this.workflows.get(workflowId);
|
|
1205
|
-
if (!workflow)
|
|
1206
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1207
|
-
await this.executeValidation(workflowId);
|
|
1208
|
-
}
|
|
1209
|
-
/**
|
|
1210
|
-
* Execute merge phase
|
|
1211
|
-
*/
|
|
1212
|
-
async executeMerge(workflowId, providerOverride) {
|
|
1213
|
-
const workflow = this.workflows.get(workflowId);
|
|
1214
|
-
if (!workflow)
|
|
1215
|
-
return;
|
|
1216
|
-
await this.transitionToPhase(workflowId, 'merging');
|
|
1217
|
-
let executionId;
|
|
1218
|
-
try {
|
|
1219
|
-
log.info('AI merging changes', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1220
|
-
const mergeList = workflow.executionIds?.['merging'] || [];
|
|
1221
|
-
const attempt = mergeList.length + 1;
|
|
1222
|
-
executionId = generateId('merge');
|
|
1223
|
-
const startTime = new Date().toISOString();
|
|
1224
|
-
workflow.executionIds = workflow.executionIds || {};
|
|
1225
|
-
workflow.executionIds['merging'] = [...mergeList, executionId];
|
|
1226
|
-
workflow.timeline = this.buildTimeline(workflow, {
|
|
1227
|
-
id: executionId,
|
|
1228
|
-
label: attempt > 1 ? `Merge – Retry #${attempt}` : 'Merge',
|
|
1229
|
-
phase: 'merging',
|
|
1230
|
-
attempt,
|
|
1231
|
-
executionId,
|
|
1232
|
-
startTime,
|
|
1233
|
-
});
|
|
1234
|
-
workflow.lastUpdatedAt = startTime;
|
|
1235
|
-
await this.saveWorkflow(workflow);
|
|
1236
|
-
this.emit('workflowUpdated', workflow);
|
|
1237
|
-
this.emit('workflowExecutionStarted', {
|
|
1238
|
-
workflowId,
|
|
1239
|
-
executionId,
|
|
1240
|
-
phase: 'merging',
|
|
1241
|
-
workflow,
|
|
1242
|
-
});
|
|
1243
|
-
await this.agentService.aiMerge(workflow.taskId, {
|
|
1244
|
-
baseBranch: 'main',
|
|
1245
|
-
executionId,
|
|
1246
|
-
workflowId,
|
|
1247
|
-
overrides: providerOverride || workflow.metadata?.aiRoutingOverrides?.ai_merge,
|
|
1248
|
-
});
|
|
1249
|
-
const mergeExec = this.agentService.getExecutionStatus(executionId);
|
|
1250
|
-
if (!mergeExec || mergeExec.status !== 'completed') {
|
|
1251
|
-
throw new Error(`AI merge failed: ${mergeExec?.error || 'Unknown error'}`);
|
|
1252
|
-
}
|
|
1253
|
-
const latest = this.workflows.get(workflowId) || workflow;
|
|
1254
|
-
const timeline = (latest.timeline || []);
|
|
1255
|
-
const tMerge = timeline.find((t) => t.executionId === executionId);
|
|
1256
|
-
if (tMerge) {
|
|
1257
|
-
tMerge.endTime = mergeExec.endTime || new Date().toISOString();
|
|
1258
|
-
tMerge.usage = mergeExec.usage;
|
|
1259
|
-
latest.timeline = this.computeVisibleTimeline(latest);
|
|
1260
|
-
latest.lastUpdatedAt = new Date().toISOString();
|
|
1261
|
-
await this.saveWorkflow(latest);
|
|
1262
|
-
}
|
|
1263
|
-
await this.taskService.updateTask(workflow.taskId, { status: 'done' });
|
|
1264
|
-
await this.transitionToPhase(workflowId, 'merged');
|
|
1265
|
-
const mergedWorkflow = this.workflows.get(workflowId);
|
|
1266
|
-
if (mergedWorkflow) {
|
|
1267
|
-
mergedWorkflow.status = 'merged';
|
|
1268
|
-
mergedWorkflow.lastUpdatedAt = new Date().toISOString();
|
|
1269
|
-
await this.saveWorkflow(mergedWorkflow);
|
|
1270
|
-
}
|
|
1271
|
-
if (workflow.metadata.stepByStepMode) {
|
|
1272
|
-
log.info('Manual step mode – merge completed; waiting for Continue', { workflowId }, 'vibing-orchestrator');
|
|
1273
|
-
}
|
|
1274
|
-
else {
|
|
1275
|
-
await this.updateCleanupStatus(workflowId);
|
|
1276
|
-
}
|
|
1277
|
-
log.info('Merge completed, ready for cleanup', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1278
|
-
}
|
|
1279
|
-
catch (error) {
|
|
1280
|
-
const latest = this.workflows.get(workflowId) || workflow;
|
|
1281
|
-
if (executionId && latest) {
|
|
1282
|
-
const timeline = (latest.timeline || []);
|
|
1283
|
-
const pending = timeline.find((t) => t.executionId === executionId);
|
|
1284
|
-
if (pending && !pending.endTime) {
|
|
1285
|
-
pending.endTime = new Date().toISOString();
|
|
1286
|
-
if (!pending.reason) {
|
|
1287
|
-
pending.reason = error instanceof Error ? error.message : String(error);
|
|
1288
|
-
}
|
|
1289
|
-
latest.timeline = this.computeVisibleTimeline(latest);
|
|
1290
|
-
latest.lastUpdatedAt = new Date().toISOString();
|
|
1291
|
-
await this.saveWorkflow(latest);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
await this.handlePhaseFailure(workflowId, 'merging', error);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
async updateCleanupStatus(workflowId) {
|
|
1298
|
-
const workflow = this.workflows.get(workflowId);
|
|
1299
|
-
if (!workflow)
|
|
1300
|
-
return;
|
|
1301
|
-
await this.transitionToPhase(workflowId, 'cleaning');
|
|
1302
|
-
workflow.status = 'cleaning';
|
|
1303
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1304
|
-
workflow.timeline = this.buildTimeline(workflow, {
|
|
1305
|
-
id: `cleaning:${new Date().toISOString()}`,
|
|
1306
|
-
label: 'Cleaning',
|
|
1307
|
-
phase: 'cleaning',
|
|
1308
|
-
startTime: new Date().toISOString(),
|
|
1309
|
-
});
|
|
1310
|
-
await this.saveWorkflow(workflow);
|
|
1311
|
-
this.emit('workflowUpdated', workflow);
|
|
1312
|
-
}
|
|
1313
|
-
/**
|
|
1314
|
-
* Public: run merge phase on demand
|
|
1315
|
-
* This will attempt merge regardless of current phase.
|
|
1316
|
-
*/
|
|
1317
|
-
async runMerge(workflowId, options) {
|
|
1318
|
-
const workflow = this.workflows.get(workflowId);
|
|
1319
|
-
if (!workflow)
|
|
1320
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1321
|
-
await this.executeMerge(workflowId, { provider: options?.provider, model: options?.model });
|
|
1322
|
-
}
|
|
1323
|
-
/**
|
|
1324
|
-
* Execute cleanup phase: remove worktree/branch and finalize workflow
|
|
1325
|
-
*/
|
|
1326
|
-
async executeCleanup(workflowId) {
|
|
1327
|
-
const workflow = this.workflows.get(workflowId);
|
|
1328
|
-
if (!workflow)
|
|
1329
|
-
return;
|
|
1330
|
-
await this.transitionToPhase(workflowId, 'cleaning');
|
|
1331
|
-
try {
|
|
1332
|
-
log.info('Cleaning up resources', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1333
|
-
// Remove worktree and local branch if present
|
|
1334
|
-
try {
|
|
1335
|
-
await this.worktreeService.deleteWorktree(workflow.taskId, true);
|
|
1336
|
-
}
|
|
1337
|
-
catch (err) {
|
|
1338
|
-
log.warn('Cleanup encountered an issue (continuing)', err, 'vibing-orchestrator');
|
|
1339
|
-
}
|
|
1340
|
-
// Finalize workflow
|
|
1341
|
-
workflow.status = 'cleaned';
|
|
1342
|
-
workflow.endTime = new Date().toISOString();
|
|
1343
|
-
await this.transitionToPhase(workflowId, 'cleaned');
|
|
1344
|
-
workflow.status = 'completed';
|
|
1345
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1346
|
-
await this.saveWorkflow(workflow);
|
|
1347
|
-
this.emit('workflowCompleted', workflow);
|
|
1348
|
-
log.info('Workflow completed', { taskId: workflow.taskId }, 'vibing-orchestrator');
|
|
1349
|
-
}
|
|
1350
|
-
catch (error) {
|
|
1351
|
-
log.error('Cleanup failed', error, 'vibing-orchestrator');
|
|
1352
|
-
workflow.error = error instanceof Error ? error.message : String(error);
|
|
1353
|
-
workflow.failureContext = {
|
|
1354
|
-
atPhase: 'cleaning',
|
|
1355
|
-
error: workflow.error,
|
|
1356
|
-
timestamp: new Date().toISOString(),
|
|
1357
|
-
};
|
|
1358
|
-
await this.transitionToPhase(workflowId, 'failed');
|
|
1359
|
-
workflow.status = 'failed';
|
|
1360
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1361
|
-
await this.saveWorkflow(workflow);
|
|
1362
|
-
this.emit('workflowFailed', workflow);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
/**
|
|
1366
|
-
* Public: run cleanup phase on demand
|
|
1367
|
-
*/
|
|
1368
|
-
async runCleanup(workflowId) {
|
|
1369
|
-
const workflow = this.workflows.get(workflowId);
|
|
1370
|
-
if (!workflow)
|
|
1371
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1372
|
-
await this.executeCleanup(workflowId);
|
|
1373
|
-
}
|
|
1374
|
-
/**
|
|
1375
|
-
* Public: manually skip to next phase
|
|
1376
|
-
*/
|
|
1377
|
-
async skipToNextPhase(workflowId) {
|
|
1378
|
-
const workflow = this.workflows.get(workflowId);
|
|
1379
|
-
if (!workflow)
|
|
1380
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1381
|
-
// If anything is currently running for this task, stop it to move on quickly
|
|
1382
|
-
try {
|
|
1383
|
-
const executions = this.agentService.getTaskExecutions(workflow.taskId) || [];
|
|
1384
|
-
const running = executions
|
|
1385
|
-
.filter((e) => e.status === 'running')
|
|
1386
|
-
.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())[0];
|
|
1387
|
-
if (running?.id) {
|
|
1388
|
-
await this.agentService.stopExecution(running.id);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
catch (error) {
|
|
1392
|
-
log.warn('Failed to stop execution before skipping phase', error, 'vibing-orchestrator');
|
|
1393
|
-
}
|
|
1394
|
-
// Determine the base phase to compute the "next" from.
|
|
1395
|
-
// For paused/failed, use the last known operational phase.
|
|
1396
|
-
const lastTransitionTo = workflow.phaseHistory?.[workflow.phaseHistory.length - 1]?.to;
|
|
1397
|
-
const basePhase = workflow.phase === 'paused'
|
|
1398
|
-
? lastTransitionTo || workflow.lastPhase || 'draft'
|
|
1399
|
-
: workflow.phase === 'failed'
|
|
1400
|
-
? (workflow.failureContext?.atPhase ||
|
|
1401
|
-
workflow.lastPhase ||
|
|
1402
|
-
lastTransitionTo ||
|
|
1403
|
-
'implementing')
|
|
1404
|
-
: workflow.phase;
|
|
1405
|
-
const ordered = [
|
|
1406
|
-
'draft',
|
|
1407
|
-
'implementing',
|
|
1408
|
-
'validating',
|
|
1409
|
-
'ai-reviewing',
|
|
1410
|
-
'awaiting-review',
|
|
1411
|
-
'approved',
|
|
1412
|
-
'merging',
|
|
1413
|
-
'merged',
|
|
1414
|
-
'cleaning',
|
|
1415
|
-
'completed',
|
|
1416
|
-
// 'failed' and 'paused' are non-linear; excluded from the sequence
|
|
1417
|
-
];
|
|
1418
|
-
const idx = ordered.indexOf(basePhase);
|
|
1419
|
-
const nextPhase = idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : 'completed';
|
|
1420
|
-
if (nextPhase && nextPhase !== workflow.phase) {
|
|
1421
|
-
log.info('Manually skipping workflow phase', { workflowId, currentPhase: workflow.phase, basePhase, nextPhase }, 'vibing-orchestrator');
|
|
1422
|
-
await this.transitionToPhase(workflowId, nextPhase);
|
|
1423
|
-
// Immediately kick off the next phase where applicable to make skipping faster
|
|
1424
|
-
switch (nextPhase) {
|
|
1425
|
-
case 'implementing':
|
|
1426
|
-
await this.executeImplementation(workflowId);
|
|
1427
|
-
break;
|
|
1428
|
-
case 'validating':
|
|
1429
|
-
await this.executeValidation(workflowId);
|
|
1430
|
-
break;
|
|
1431
|
-
case 'ai-reviewing':
|
|
1432
|
-
await this.executeAiReviewPhase(workflowId);
|
|
1433
|
-
break;
|
|
1434
|
-
case 'approved':
|
|
1435
|
-
case 'merging':
|
|
1436
|
-
await this.executeMerge(workflowId);
|
|
1437
|
-
break;
|
|
1438
|
-
case 'merged':
|
|
1439
|
-
case 'cleaning':
|
|
1440
|
-
await this.executeCleanup(workflowId);
|
|
1441
|
-
break;
|
|
1442
|
-
case 'awaiting-review':
|
|
1443
|
-
case 'completed':
|
|
1444
|
-
default:
|
|
1445
|
-
// No immediate action; awaiting-review waits for human approval; completed is terminal
|
|
1446
|
-
break;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
/**
|
|
1451
|
-
* Wait for an execution to complete
|
|
1452
|
-
*/
|
|
1453
|
-
async waitForExecution(executionId, timeoutMs = 30 * 60 * 1000) {
|
|
1454
|
-
const startTime = Date.now();
|
|
1455
|
-
let missingStatusWarned = false;
|
|
1456
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
1457
|
-
const execution = this.agentService.getExecutionStatus(executionId);
|
|
1458
|
-
if (!execution) {
|
|
1459
|
-
if (!missingStatusWarned) {
|
|
1460
|
-
log.debug('Execution status not yet available; waiting for agent to register', { executionId }, 'vibing-orchestrator');
|
|
1461
|
-
missingStatusWarned = true;
|
|
1462
|
-
}
|
|
1463
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1464
|
-
continue;
|
|
1465
|
-
}
|
|
1466
|
-
missingStatusWarned = false;
|
|
1467
|
-
if (['completed', 'failed', 'cancelled'].includes(execution.status)) {
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
// Wait 5 seconds before checking again
|
|
1471
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1472
|
-
}
|
|
1473
|
-
throw new Error(`Execution ${executionId} timed out after ${timeoutMs}ms`);
|
|
1474
|
-
}
|
|
1475
|
-
/**
|
|
1476
|
-
* Wait for the AgentService to emit an executionCreated event for the given task/workflow
|
|
1477
|
-
*/
|
|
1478
|
-
waitForExecutionCreated(taskId, workflowId, timeoutMs = 15000) {
|
|
1479
|
-
return new Promise((resolve, reject) => {
|
|
1480
|
-
const onCreated = (data) => {
|
|
1481
|
-
try {
|
|
1482
|
-
if (data.taskId !== taskId)
|
|
1483
|
-
return;
|
|
1484
|
-
if (workflowId && data.workflowId !== workflowId)
|
|
1485
|
-
return;
|
|
1486
|
-
cleanup();
|
|
1487
|
-
resolve(data.executionId);
|
|
1488
|
-
}
|
|
1489
|
-
catch (err) {
|
|
1490
|
-
cleanup();
|
|
1491
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1492
|
-
}
|
|
1493
|
-
};
|
|
1494
|
-
const cleanup = () => {
|
|
1495
|
-
clearTimeout(timer);
|
|
1496
|
-
this.agentService.off('executionCreated', onCreated);
|
|
1497
|
-
};
|
|
1498
|
-
const timer = setTimeout(() => {
|
|
1499
|
-
cleanup();
|
|
1500
|
-
reject(new Error('Timed out waiting for executionCreated event'));
|
|
1501
|
-
}, timeoutMs);
|
|
1502
|
-
this.agentService.on('executionCreated', onCreated);
|
|
1503
|
-
});
|
|
1504
|
-
}
|
|
1505
|
-
/**
|
|
1506
|
-
* Get workflow statistics
|
|
1507
|
-
*/
|
|
1508
|
-
getWorkflowStats() {
|
|
1509
|
-
const workflows = Array.from(this.workflows.values());
|
|
1510
|
-
const stats = {
|
|
1511
|
-
total: workflows.length,
|
|
1512
|
-
byPhase: {},
|
|
1513
|
-
active: 0,
|
|
1514
|
-
completed: 0,
|
|
1515
|
-
failed: 0,
|
|
1516
|
-
};
|
|
1517
|
-
// Initialize phase counts
|
|
1518
|
-
const phases = [
|
|
1519
|
-
'draft',
|
|
1520
|
-
'implementing',
|
|
1521
|
-
'validating',
|
|
1522
|
-
'ai-reviewing',
|
|
1523
|
-
'awaiting-review',
|
|
1524
|
-
'paused',
|
|
1525
|
-
'approved',
|
|
1526
|
-
'merging',
|
|
1527
|
-
'merged',
|
|
1528
|
-
'cleaning',
|
|
1529
|
-
'completed',
|
|
1530
|
-
'failed',
|
|
1531
|
-
];
|
|
1532
|
-
phases.forEach((phase) => {
|
|
1533
|
-
stats.byPhase[phase] = 0;
|
|
1534
|
-
});
|
|
1535
|
-
// Count workflows by phase
|
|
1536
|
-
workflows.forEach((workflow) => {
|
|
1537
|
-
stats.byPhase[workflow.phase]++;
|
|
1538
|
-
if ([
|
|
1539
|
-
'implementing',
|
|
1540
|
-
'validating',
|
|
1541
|
-
'ai-reviewing',
|
|
1542
|
-
'awaiting-review',
|
|
1543
|
-
'approved',
|
|
1544
|
-
'merging',
|
|
1545
|
-
'cleaning',
|
|
1546
|
-
].includes(workflow.phase)) {
|
|
1547
|
-
stats.active++;
|
|
1548
|
-
}
|
|
1549
|
-
else if (['completed', 'merged'].includes(workflow.phase)) {
|
|
1550
|
-
stats.completed++;
|
|
1551
|
-
}
|
|
1552
|
-
else if (workflow.phase === 'failed') {
|
|
1553
|
-
stats.failed++;
|
|
1554
|
-
}
|
|
1555
|
-
});
|
|
1556
|
-
return stats;
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Get workflow persistence information
|
|
1560
|
-
*/
|
|
1561
|
-
getWorkflowPersistenceInfo() {
|
|
1562
|
-
return {
|
|
1563
|
-
filePath: this.workflowDB.getFilePath(),
|
|
1564
|
-
workflowCount: this.workflows.size,
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* Force save all workflows to disk
|
|
1569
|
-
*/
|
|
1570
|
-
async saveAllWorkflows() {
|
|
1571
|
-
const obj = {};
|
|
1572
|
-
for (const [id, wf] of this.workflows)
|
|
1573
|
-
obj[id] = serializeVibe(wf);
|
|
1574
|
-
await this.workflowDB.setAll(obj);
|
|
1575
|
-
}
|
|
1576
|
-
/**
|
|
1577
|
-
* Reload workflows from disk
|
|
1578
|
-
*/
|
|
1579
|
-
async reloadWorkflows() {
|
|
1580
|
-
const all = await this.workflowDB.getAll();
|
|
1581
|
-
const map = new Map();
|
|
1582
|
-
for (const [id, wf] of Object.entries(all)) {
|
|
1583
|
-
map.set(id, this.normalizeWorkflow(wf));
|
|
1584
|
-
}
|
|
1585
|
-
this.workflows = map;
|
|
1586
|
-
log.info('Reloaded workflows from disk', { count: this.workflows.size }, 'vibing-orchestrator');
|
|
1587
|
-
}
|
|
1588
|
-
/**
|
|
1589
|
-
* Remove workflows by task ID
|
|
1590
|
-
*/
|
|
1591
|
-
async removeWorkflowsByTask(taskId) {
|
|
1592
|
-
const all = await this.workflowDB.getAll();
|
|
1593
|
-
let removedCount = 0;
|
|
1594
|
-
for (const [id, workflow] of Object.entries(all)) {
|
|
1595
|
-
if (workflow.taskId === taskId) {
|
|
1596
|
-
delete all[id];
|
|
1597
|
-
removedCount++;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
if (removedCount > 0) {
|
|
1601
|
-
await this.workflowDB.setAll(all);
|
|
1602
|
-
}
|
|
1603
|
-
// Also remove from memory
|
|
1604
|
-
for (const [id, workflow] of this.workflows) {
|
|
1605
|
-
if (workflow.taskId === taskId) {
|
|
1606
|
-
this.workflows.delete(id);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
return removedCount;
|
|
1610
|
-
}
|
|
1611
|
-
/**
|
|
1612
|
-
* Get comprehensive workflow statistics
|
|
1613
|
-
*/
|
|
1614
|
-
async getDetailedStats() {
|
|
1615
|
-
return {
|
|
1616
|
-
memoryStats: this.getWorkflowStats(),
|
|
1617
|
-
persistenceStats: await getDbStats(this.workflowDB),
|
|
1618
|
-
persistencePath: this.workflowDB.getFilePath(),
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
/**
|
|
1622
|
-
* Update workflow options without changing phase
|
|
1623
|
-
*/
|
|
1624
|
-
async updateWorkflowOptions(workflowId, config) {
|
|
1625
|
-
const workflow = this.workflows.get(workflowId);
|
|
1626
|
-
if (!workflow) {
|
|
1627
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1628
|
-
}
|
|
1629
|
-
// Update metadata with new config
|
|
1630
|
-
workflow.metadata = { ...workflow.metadata, ...config };
|
|
1631
|
-
// Persist the updated workflow
|
|
1632
|
-
await this.saveWorkflow(workflow);
|
|
1633
|
-
log.info('Updated workflow options', { workflowId }, 'vibing-orchestrator');
|
|
1634
|
-
this.emit('workflowOptionsUpdated', workflow);
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* Continue workflow execution from current state
|
|
1638
|
-
*/
|
|
1639
|
-
async continueWorkflow(workflowId) {
|
|
1640
|
-
const workflow = this.workflows.get(workflowId);
|
|
1641
|
-
if (!workflow) {
|
|
1642
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1643
|
-
}
|
|
1644
|
-
log.info('Continuing workflow (status-driven)', { workflowId, phase: workflow.phase, status: workflow.status }, 'vibing-orchestrator');
|
|
1645
|
-
// If failed, prefer immediate retry of implementation
|
|
1646
|
-
if (workflow.phase === 'failed' || workflow.status === 'failed') {
|
|
1647
|
-
if (workflow.metadata.stepByStepMode) {
|
|
1648
|
-
await this.rerunImplementation(workflowId);
|
|
1649
|
-
}
|
|
1650
|
-
else {
|
|
1651
|
-
await this.executeImplementation(workflowId);
|
|
1652
|
-
}
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
// Compute next actionable phase from status/checkpoint
|
|
1656
|
-
const next = this.computeNextActionPhase(workflow);
|
|
1657
|
-
if (!next) {
|
|
1658
|
-
log.info('No actionable next phase (waiting for human or already done)', { workflowId, phase: workflow.phase, status: workflow.status }, 'vibing-orchestrator');
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
switch (next) {
|
|
1662
|
-
case 'implementing':
|
|
1663
|
-
await this.executeImplementation(workflowId);
|
|
1664
|
-
return;
|
|
1665
|
-
case 'validating':
|
|
1666
|
-
await this.executeValidation(workflowId);
|
|
1667
|
-
return;
|
|
1668
|
-
case 'ai-reviewing':
|
|
1669
|
-
await this.executeAiReviewPhase(workflowId);
|
|
1670
|
-
return;
|
|
1671
|
-
case 'awaiting-review':
|
|
1672
|
-
await this.executeAwaitingReview(workflowId);
|
|
1673
|
-
return;
|
|
1674
|
-
case 'merging':
|
|
1675
|
-
await this.executeMerge(workflowId);
|
|
1676
|
-
return;
|
|
1677
|
-
case 'cleaning':
|
|
1678
|
-
await this.executeCleanup(workflowId);
|
|
1679
|
-
return;
|
|
1680
|
-
default:
|
|
1681
|
-
throw new Error(`Unsupported next phase computed: ${next}`);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
// Decide next action from status/checkpoint and configuration
|
|
1685
|
-
computeNextActionPhase(workflow) {
|
|
1686
|
-
const status = workflow.status;
|
|
1687
|
-
const cfg = workflow.metadata || {};
|
|
1688
|
-
const requireHuman = !!cfg.requireHumanApproval;
|
|
1689
|
-
const wantsAIReview = !!cfg.aiCodeReview;
|
|
1690
|
-
// If paused, compute by last checkpoint
|
|
1691
|
-
// If failed handling is done earlier
|
|
1692
|
-
switch (status) {
|
|
1693
|
-
case undefined:
|
|
1694
|
-
case 'draft':
|
|
1695
|
-
return 'implementing';
|
|
1696
|
-
case 'implemented':
|
|
1697
|
-
return 'validating';
|
|
1698
|
-
case 'validated':
|
|
1699
|
-
if (wantsAIReview)
|
|
1700
|
-
return 'ai-reviewing';
|
|
1701
|
-
return requireHuman ? null : 'merging';
|
|
1702
|
-
case 'ai-reviewed':
|
|
1703
|
-
return requireHuman ? null : 'merging';
|
|
1704
|
-
case 'awaiting-review':
|
|
1705
|
-
return null; // wait for approveWorkflow
|
|
1706
|
-
case 'approved':
|
|
1707
|
-
return 'merging';
|
|
1708
|
-
case 'merged':
|
|
1709
|
-
return 'cleaning';
|
|
1710
|
-
case 'cleaned':
|
|
1711
|
-
return 'completed';
|
|
1712
|
-
case 'completed':
|
|
1713
|
-
return null;
|
|
1714
|
-
case 'paused':
|
|
1715
|
-
// Map paused to next by lastPhase or default to implementing
|
|
1716
|
-
return workflow.lastPhase || 'implementing';
|
|
1717
|
-
case 'failed':
|
|
1718
|
-
return 'implementing';
|
|
1719
|
-
default:
|
|
1720
|
-
return null;
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
// ============ Small helpers to reduce duplication ============
|
|
1724
|
-
requireWorkflow(workflowId) {
|
|
1725
|
-
const wf = this.workflows.get(workflowId);
|
|
1726
|
-
if (!wf)
|
|
1727
|
-
throw new Error(`Workflow ${workflowId} not found`);
|
|
1728
|
-
return wf;
|
|
1729
|
-
}
|
|
1730
|
-
async getAIReviewThreshold() {
|
|
1731
|
-
try {
|
|
1732
|
-
const settingsService = getSettingsService();
|
|
1733
|
-
await settingsService.initialize();
|
|
1734
|
-
const settings = settingsService.getSettings();
|
|
1735
|
-
return settings.agents.judgeAgent.reviewThresholdScore;
|
|
1736
|
-
}
|
|
1737
|
-
catch (error) {
|
|
1738
|
-
log.warn('Failed to load AI review threshold from settings, using default', error, 'vibing-orchestrator');
|
|
1739
|
-
return 70; // fallback default
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
assertPhase(workflow, expected, action) {
|
|
1743
|
-
if (workflow.phase !== expected) {
|
|
1744
|
-
throw new Error(`Cannot ${action} workflow in phase ${workflow.phase}; expected ${expected}`);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
createNewWorkflow(workflowId, taskId, config) {
|
|
1748
|
-
const now = new Date().toISOString();
|
|
1749
|
-
const wf = {
|
|
1750
|
-
id: workflowId,
|
|
1751
|
-
taskId,
|
|
1752
|
-
phase: 'draft',
|
|
1753
|
-
startTime: now,
|
|
1754
|
-
endTime: undefined,
|
|
1755
|
-
status: 'draft',
|
|
1756
|
-
// Phase tracking
|
|
1757
|
-
lastPhase: undefined,
|
|
1758
|
-
phaseHistory: [],
|
|
1759
|
-
failureContext: undefined,
|
|
1760
|
-
// Execution tracking and attempts per phase
|
|
1761
|
-
executionIds: {
|
|
1762
|
-
implementing: [],
|
|
1763
|
-
validating: [],
|
|
1764
|
-
'ai-reviewing': [],
|
|
1765
|
-
merging: [],
|
|
1766
|
-
},
|
|
1767
|
-
attempts: {},
|
|
1768
|
-
// Rerun context audit trail
|
|
1769
|
-
rerunContextHistory: [],
|
|
1770
|
-
// Quality + links placeholders
|
|
1771
|
-
qualityResults: undefined,
|
|
1772
|
-
links: {},
|
|
1773
|
-
// Error placeholder
|
|
1774
|
-
error: undefined,
|
|
1775
|
-
// Observability/metrics
|
|
1776
|
-
metrics: { durationsMs: {}, lastPhaseStartedAt: now },
|
|
1777
|
-
lastUpdatedAt: now,
|
|
1778
|
-
// Persisted workflow configuration
|
|
1779
|
-
metadata: config,
|
|
1780
|
-
// Initial timeline marker
|
|
1781
|
-
timeline: [
|
|
1782
|
-
{
|
|
1783
|
-
id: 'start',
|
|
1784
|
-
label: 'Workflow started',
|
|
1785
|
-
phase: 'draft',
|
|
1786
|
-
startTime: now,
|
|
1787
|
-
},
|
|
1788
|
-
],
|
|
1789
|
-
};
|
|
1790
|
-
// Seed placeholders for the rest of the flow
|
|
1791
|
-
wf.timeline = this.computeVisibleTimeline(wf);
|
|
1792
|
-
return wf;
|
|
1793
|
-
}
|
|
1794
|
-
async maybeRetryImplementation(workflowId, reason, _atPhase) {
|
|
1795
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
1796
|
-
// In step-by-step mode, do not auto-retry; wait for explicit Continue
|
|
1797
|
-
if (workflow.metadata.stepByStepMode) {
|
|
1798
|
-
log.info('Step-by-step mode: skipping auto-retry of implementation', { workflowId, reason }, 'vibing-orchestrator');
|
|
1799
|
-
return false;
|
|
1800
|
-
}
|
|
1801
|
-
const max = workflow.metadata.retryPolicy?.maxImplementationAttempts || 0;
|
|
1802
|
-
const implementingCount = (workflow.phaseHistory || []).filter((h) => h.to === 'implementing').length;
|
|
1803
|
-
if (max > 0 && implementingCount < max) {
|
|
1804
|
-
await this.transitionToPhase(workflowId, 'implementing');
|
|
1805
|
-
await this.executeImplementation(workflowId, reason);
|
|
1806
|
-
return true;
|
|
1807
|
-
}
|
|
1808
|
-
return false;
|
|
1809
|
-
}
|
|
1810
|
-
async handlePhaseFailure(workflowId, atPhase, error) {
|
|
1811
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
1812
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1813
|
-
log.error(`${atPhase} failed`, error, 'vibing-orchestrator');
|
|
1814
|
-
workflow.error = msg;
|
|
1815
|
-
workflow.failureContext = {
|
|
1816
|
-
atPhase,
|
|
1817
|
-
error: msg,
|
|
1818
|
-
timestamp: new Date().toISOString(),
|
|
1819
|
-
};
|
|
1820
|
-
// Attach reason to last timeline item of this phase
|
|
1821
|
-
const wf = this.workflows.get(workflowId);
|
|
1822
|
-
if (wf && Array.isArray(wf.timeline)) {
|
|
1823
|
-
for (let i = wf.timeline.length - 1; i >= 0; i--) {
|
|
1824
|
-
const t = wf.timeline[i];
|
|
1825
|
-
if (t.phase === atPhase) {
|
|
1826
|
-
t.reason = msg;
|
|
1827
|
-
break;
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
await this.saveWorkflow(wf);
|
|
1831
|
-
}
|
|
1832
|
-
await this.transitionToPhase(workflowId, 'failed');
|
|
1833
|
-
workflow.status = 'failed';
|
|
1834
|
-
workflow.lastUpdatedAt = new Date().toISOString();
|
|
1835
|
-
await this.saveWorkflow(workflow);
|
|
1836
|
-
this.emit('workflowFailed', workflow);
|
|
1837
|
-
}
|
|
1838
|
-
/**
|
|
1839
|
-
* Get the agent service instance
|
|
1840
|
-
*/
|
|
1841
|
-
getAgentService() {
|
|
1842
|
-
return this.agentService;
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
// Helpers local to this module
|
|
1846
|
-
function serializeVibe(workflow) {
|
|
1847
|
-
return {
|
|
1848
|
-
...workflow,
|
|
1849
|
-
startTime: new Date(workflow.startTime).toISOString(),
|
|
1850
|
-
endTime: workflow.endTime ? new Date(workflow.endTime).toISOString() : undefined,
|
|
1851
|
-
qualityResults: workflow.qualityResults
|
|
1852
|
-
? {
|
|
1853
|
-
...workflow.qualityResults,
|
|
1854
|
-
timestamp: new Date(workflow.qualityResults.timestamp).toISOString(),
|
|
1855
|
-
}
|
|
1856
|
-
: undefined,
|
|
1857
|
-
aiReviewResult: workflow.aiReviewResult
|
|
1858
|
-
? {
|
|
1859
|
-
...workflow.aiReviewResult,
|
|
1860
|
-
timestamp: workflow.aiReviewResult.timestamp
|
|
1861
|
-
? new Date(workflow.aiReviewResult.timestamp).toISOString()
|
|
1862
|
-
: undefined,
|
|
1863
|
-
}
|
|
1864
|
-
: undefined,
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
async function getDbStats(db) {
|
|
1868
|
-
const all = await db.getAll();
|
|
1869
|
-
const stats = {
|
|
1870
|
-
total: 0,
|
|
1871
|
-
byPhase: {},
|
|
1872
|
-
oldestWorkflow: undefined,
|
|
1873
|
-
newestWorkflow: undefined,
|
|
1874
|
-
};
|
|
1875
|
-
let oldest = null;
|
|
1876
|
-
let newest = null;
|
|
1877
|
-
for (const wf of Object.values(all)) {
|
|
1878
|
-
stats.total += 1;
|
|
1879
|
-
stats.byPhase[wf.phase] = (stats.byPhase[wf.phase] || 0) + 1;
|
|
1880
|
-
const start = new Date(wf.startTime);
|
|
1881
|
-
if (!oldest || start < oldest) {
|
|
1882
|
-
oldest = start;
|
|
1883
|
-
stats.oldestWorkflow = wf.id;
|
|
1884
|
-
}
|
|
1885
|
-
if (!newest || start > newest) {
|
|
1886
|
-
newest = start;
|
|
1887
|
-
stats.newestWorkflow = wf.id;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
return stats;
|
|
1891
|
-
}
|