@litmers/cursorflow-orchestrator 0.1.13 → 0.1.15
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/CHANGELOG.md +37 -0
- package/README.md +83 -2
- package/commands/cursorflow-clean.md +20 -6
- package/commands/cursorflow-prepare.md +1 -1
- package/commands/cursorflow-resume.md +127 -6
- package/commands/cursorflow-run.md +2 -2
- package/commands/cursorflow-signal.md +11 -4
- package/dist/cli/clean.js +164 -12
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +8 -0
- package/dist/cli/logs.js +759 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/monitor.js +113 -30
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +1 -1
- package/dist/cli/resume.js +367 -18
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +9 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +34 -20
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +13 -1
- package/dist/core/orchestrator.js +396 -35
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +24 -2
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +266 -61
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +38 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +210 -0
- package/dist/utils/enhanced-logger.js +1030 -0
- package/dist/utils/enhanced-logger.js.map +1 -0
- package/dist/utils/events.d.ts +59 -0
- package/dist/utils/events.js +37 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/git.d.ts +11 -0
- package/dist/utils/git.js +40 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/types.d.ts +132 -1
- package/dist/utils/webhook.d.ts +5 -0
- package/dist/utils/webhook.js +109 -0
- package/dist/utils/webhook.js.map +1 -0
- package/examples/README.md +1 -1
- package/package.json +2 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/simple-logging-test.sh +97 -0
- package/scripts/test-real-cursor-lifecycle.sh +289 -0
- package/scripts/test-real-logging.sh +289 -0
- package/scripts/test-streaming-multi-task.sh +247 -0
- package/src/cli/clean.ts +170 -13
- package/src/cli/index.ts +4 -1
- package/src/cli/logs.ts +863 -0
- package/src/cli/monitor.ts +123 -30
- package/src/cli/prepare.ts +1 -1
- package/src/cli/resume.ts +463 -22
- package/src/cli/run.ts +10 -0
- package/src/cli/signal.ts +43 -27
- package/src/core/orchestrator.ts +458 -36
- package/src/core/reviewer.ts +40 -4
- package/src/core/runner.ts +293 -60
- package/src/utils/config.ts +41 -1
- package/src/utils/enhanced-logger.ts +1166 -0
- package/src/utils/events.ts +117 -0
- package/src/utils/git.ts +40 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/types.ts +160 -1
- package/src/utils/webhook.ts +85 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import {
|
|
3
|
+
CursorFlowEvent,
|
|
4
|
+
EventHandler,
|
|
5
|
+
OrchestrationStartedPayload,
|
|
6
|
+
OrchestrationCompletedPayload,
|
|
7
|
+
OrchestrationFailedPayload,
|
|
8
|
+
LaneStartedPayload,
|
|
9
|
+
LaneCompletedPayload,
|
|
10
|
+
LaneFailedPayload,
|
|
11
|
+
LaneDependencyRequestedPayload,
|
|
12
|
+
TaskStartedPayload,
|
|
13
|
+
TaskCompletedPayload,
|
|
14
|
+
TaskFailedPayload,
|
|
15
|
+
AgentPromptSentPayload,
|
|
16
|
+
AgentResponseReceivedPayload,
|
|
17
|
+
ReviewStartedPayload,
|
|
18
|
+
ReviewCompletedPayload,
|
|
19
|
+
ReviewApprovedPayload,
|
|
20
|
+
ReviewRejectedPayload
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
class CursorFlowEvents extends EventEmitter {
|
|
24
|
+
private runId: string = '';
|
|
25
|
+
|
|
26
|
+
setRunId(id: string) {
|
|
27
|
+
this.runId = id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Specific event overloads for emit
|
|
31
|
+
emit(type: 'orchestration.started', payload: OrchestrationStartedPayload): boolean;
|
|
32
|
+
emit(type: 'orchestration.completed', payload: OrchestrationCompletedPayload): boolean;
|
|
33
|
+
emit(type: 'orchestration.failed', payload: OrchestrationFailedPayload): boolean;
|
|
34
|
+
emit(type: 'lane.started', payload: LaneStartedPayload): boolean;
|
|
35
|
+
emit(type: 'lane.completed', payload: LaneCompletedPayload): boolean;
|
|
36
|
+
emit(type: 'lane.failed', payload: LaneFailedPayload): boolean;
|
|
37
|
+
emit(type: 'lane.dependency_requested', payload: LaneDependencyRequestedPayload): boolean;
|
|
38
|
+
emit(type: 'task.started', payload: TaskStartedPayload): boolean;
|
|
39
|
+
emit(type: 'task.completed', payload: TaskCompletedPayload): boolean;
|
|
40
|
+
emit(type: 'task.failed', payload: TaskFailedPayload): boolean;
|
|
41
|
+
emit(type: 'agent.prompt_sent', payload: AgentPromptSentPayload): boolean;
|
|
42
|
+
emit(type: 'agent.response_received', payload: AgentResponseReceivedPayload): boolean;
|
|
43
|
+
emit(type: 'review.started', payload: ReviewStartedPayload): boolean;
|
|
44
|
+
emit(type: 'review.completed', payload: ReviewCompletedPayload): boolean;
|
|
45
|
+
emit(type: 'review.approved', payload: ReviewApprovedPayload): boolean;
|
|
46
|
+
emit(type: 'review.rejected', payload: ReviewRejectedPayload): boolean;
|
|
47
|
+
emit(type: string, payload: any): boolean;
|
|
48
|
+
emit(type: string, payload: any): boolean {
|
|
49
|
+
const event: CursorFlowEvent = {
|
|
50
|
+
id: `evt_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
51
|
+
type,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
runId: this.runId,
|
|
54
|
+
payload,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Emit specific event
|
|
58
|
+
super.emit(type, event);
|
|
59
|
+
|
|
60
|
+
// Emit wildcard patterns (e.g., 'task.*' listeners)
|
|
61
|
+
const parts = type.split('.');
|
|
62
|
+
if (parts.length > 1) {
|
|
63
|
+
const category = parts[0];
|
|
64
|
+
super.emit(`${category}.*`, event);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
super.emit('*', event);
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Specific event overloads for on
|
|
73
|
+
on(pattern: 'orchestration.started', handler: EventHandler<OrchestrationStartedPayload>): this;
|
|
74
|
+
on(pattern: 'orchestration.completed', handler: EventHandler<OrchestrationCompletedPayload>): this;
|
|
75
|
+
on(pattern: 'orchestration.failed', handler: EventHandler<OrchestrationFailedPayload>): this;
|
|
76
|
+
on(pattern: 'lane.started', handler: EventHandler<LaneStartedPayload>): this;
|
|
77
|
+
on(pattern: 'lane.completed', handler: EventHandler<LaneCompletedPayload>): this;
|
|
78
|
+
on(pattern: 'lane.failed', handler: EventHandler<LaneFailedPayload>): this;
|
|
79
|
+
on(pattern: 'lane.dependency_requested', handler: EventHandler<LaneDependencyRequestedPayload>): this;
|
|
80
|
+
on(pattern: 'task.started', handler: EventHandler<TaskStartedPayload>): this;
|
|
81
|
+
on(pattern: 'task.completed', handler: EventHandler<TaskCompletedPayload>): this;
|
|
82
|
+
on(pattern: 'task.failed', handler: EventHandler<TaskFailedPayload>): this;
|
|
83
|
+
on(pattern: 'agent.prompt_sent', handler: EventHandler<AgentPromptSentPayload>): this;
|
|
84
|
+
on(pattern: 'agent.response_received', handler: EventHandler<AgentResponseReceivedPayload>): this;
|
|
85
|
+
on(pattern: 'review.started', handler: EventHandler<ReviewStartedPayload>): this;
|
|
86
|
+
on(pattern: 'review.completed', handler: EventHandler<ReviewCompletedPayload>): this;
|
|
87
|
+
on(pattern: 'review.approved', handler: EventHandler<ReviewApprovedPayload>): this;
|
|
88
|
+
on(pattern: 'review.rejected', handler: EventHandler<ReviewRejectedPayload>): this;
|
|
89
|
+
on(pattern: string, handler: EventHandler): this;
|
|
90
|
+
on(pattern: string, handler: EventHandler): this {
|
|
91
|
+
return super.on(pattern, handler);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
once(pattern: 'orchestration.started', handler: EventHandler<OrchestrationStartedPayload>): this;
|
|
95
|
+
once(pattern: 'orchestration.completed', handler: EventHandler<OrchestrationCompletedPayload>): this;
|
|
96
|
+
once(pattern: 'orchestration.failed', handler: EventHandler<OrchestrationFailedPayload>): this;
|
|
97
|
+
once(pattern: 'lane.started', handler: EventHandler<LaneStartedPayload>): this;
|
|
98
|
+
once(pattern: 'lane.completed', handler: EventHandler<LaneCompletedPayload>): this;
|
|
99
|
+
once(pattern: 'lane.failed', handler: EventHandler<LaneFailedPayload>): this;
|
|
100
|
+
once(pattern: 'lane.dependency_requested', handler: EventHandler<LaneDependencyRequestedPayload>): this;
|
|
101
|
+
once(pattern: 'task.started', handler: EventHandler<TaskStartedPayload>): this;
|
|
102
|
+
once(pattern: 'task.completed', handler: EventHandler<TaskCompletedPayload>): this;
|
|
103
|
+
once(pattern: 'task.failed', handler: EventHandler<TaskFailedPayload>): this;
|
|
104
|
+
once(pattern: 'agent.prompt_sent', handler: EventHandler<AgentPromptSentPayload>): this;
|
|
105
|
+
once(pattern: 'agent.response_received', handler: EventHandler<AgentResponseReceivedPayload>): this;
|
|
106
|
+
once(pattern: 'review.started', handler: EventHandler<ReviewStartedPayload>): this;
|
|
107
|
+
once(pattern: 'review.completed', handler: EventHandler<ReviewCompletedPayload>): this;
|
|
108
|
+
once(pattern: 'review.approved', handler: EventHandler<ReviewApprovedPayload>): this;
|
|
109
|
+
once(pattern: 'review.rejected', handler: EventHandler<ReviewRejectedPayload>): this;
|
|
110
|
+
once(pattern: string, handler: EventHandler): this;
|
|
111
|
+
once(pattern: string, handler: EventHandler): this {
|
|
112
|
+
return super.once(pattern, handler);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const events = new CursorFlowEvents();
|
|
117
|
+
|
package/src/utils/git.ts
CHANGED
|
@@ -217,12 +217,27 @@ export function commit(message: string, options: { cwd?: string; addAll?: boolea
|
|
|
217
217
|
runGit(['commit', '-m', message], { cwd });
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Check if a remote exists
|
|
222
|
+
*/
|
|
223
|
+
export function remoteExists(remoteName = 'origin', options: { cwd?: string } = {}): boolean {
|
|
224
|
+
const result = runGitResult(['remote'], { cwd: options.cwd });
|
|
225
|
+
if (!result.success) return false;
|
|
226
|
+
return result.stdout.split('\n').map(r => r.trim()).includes(remoteName);
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
/**
|
|
221
230
|
* Push to remote
|
|
222
231
|
*/
|
|
223
232
|
export function push(branchName: string, options: { cwd?: string; force?: boolean; setUpstream?: boolean } = {}): void {
|
|
224
233
|
const { cwd, force = false, setUpstream = false } = options;
|
|
225
234
|
|
|
235
|
+
// Check if origin exists before pushing
|
|
236
|
+
if (!remoteExists('origin', { cwd })) {
|
|
237
|
+
// If no origin, just skip pushing (useful for local tests)
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
226
241
|
const args = ['push'];
|
|
227
242
|
|
|
228
243
|
if (force) {
|
|
@@ -323,3 +338,28 @@ export function getCommitInfo(commitHash: string, options: { cwd?: string } = {}
|
|
|
323
338
|
subject: lines[5] || '',
|
|
324
339
|
};
|
|
325
340
|
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get diff statistics for the last operation (commit or merge)
|
|
344
|
+
* Comparing HEAD with its first parent
|
|
345
|
+
*/
|
|
346
|
+
export function getLastOperationStats(cwd?: string): string {
|
|
347
|
+
try {
|
|
348
|
+
// Check if there are any commits
|
|
349
|
+
const hasCommits = runGitResult(['rev-parse', 'HEAD'], { cwd }).success;
|
|
350
|
+
if (!hasCommits) return '';
|
|
351
|
+
|
|
352
|
+
// Check if HEAD has a parent
|
|
353
|
+
const hasParent = runGitResult(['rev-parse', 'HEAD^1'], { cwd }).success;
|
|
354
|
+
if (!hasParent) {
|
|
355
|
+
// If no parent, show stats for the first commit
|
|
356
|
+
// Using an empty tree hash as the base
|
|
357
|
+
const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
358
|
+
return runGit(['diff', '--stat', emptyTree, 'HEAD'], { cwd, silent: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return runGit(['diff', '--stat', 'HEAD^1', 'HEAD'], { cwd, silent: true });
|
|
362
|
+
} catch (e) {
|
|
363
|
+
return '';
|
|
364
|
+
}
|
|
365
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -16,7 +16,9 @@ export const COLORS = {
|
|
|
16
16
|
green: '\x1b[32m',
|
|
17
17
|
blue: '\x1b[34m',
|
|
18
18
|
cyan: '\x1b[36m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
19
20
|
gray: '\x1b[90m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
let currentLogLevel: number = LogLevel.info;
|
|
@@ -38,7 +40,8 @@ export function setLogLevel(level: string | number): void {
|
|
|
38
40
|
function formatMessage(level: string, message: string, emoji = ''): string {
|
|
39
41
|
const timestamp = new Date().toISOString();
|
|
40
42
|
const prefix = emoji ? `${emoji} ` : '';
|
|
41
|
-
|
|
43
|
+
const lines = String(message).split('\n');
|
|
44
|
+
return lines.map(line => `[${timestamp}] [${level.toUpperCase()}] ${prefix}${line}`).join('\n');
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
package/src/utils/types.ts
CHANGED
|
@@ -25,6 +25,157 @@ export interface CursorFlowConfig {
|
|
|
25
25
|
worktreePrefix: string;
|
|
26
26
|
maxConcurrentLanes: number;
|
|
27
27
|
projectRoot: string;
|
|
28
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
29
|
+
agentOutputFormat: 'stream-json' | 'json' | 'plain';
|
|
30
|
+
webhooks?: WebhookConfig[];
|
|
31
|
+
/** Enhanced logging configuration */
|
|
32
|
+
enhancedLogging?: Partial<EnhancedLogConfig>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WebhookConfig {
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
url: string;
|
|
38
|
+
secret?: string;
|
|
39
|
+
events?: string[]; // ['*'] for all, ['task.*'] for wildcards
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
retries?: number;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Enhanced logging configuration
|
|
47
|
+
*/
|
|
48
|
+
export interface EnhancedLogConfig {
|
|
49
|
+
/** Enable enhanced logging features (default: true) */
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
|
|
52
|
+
/** Strip ANSI escape codes from clean logs (default: true) */
|
|
53
|
+
stripAnsi: boolean;
|
|
54
|
+
|
|
55
|
+
/** Add timestamps to each line (default: true) */
|
|
56
|
+
addTimestamps: boolean;
|
|
57
|
+
|
|
58
|
+
/** Maximum size in bytes before rotation (default: 50MB) */
|
|
59
|
+
maxFileSize: number;
|
|
60
|
+
|
|
61
|
+
/** Number of rotated files to keep (default: 5) */
|
|
62
|
+
maxFiles: number;
|
|
63
|
+
|
|
64
|
+
/** Write raw output with ANSI codes to separate file (default: true) */
|
|
65
|
+
keepRawLogs: boolean;
|
|
66
|
+
|
|
67
|
+
/** Write structured JSON log entries (default: true) */
|
|
68
|
+
writeJsonLog: boolean;
|
|
69
|
+
|
|
70
|
+
/** Timestamp format: 'iso' | 'relative' | 'short' (default: 'iso') */
|
|
71
|
+
timestampFormat: 'iso' | 'relative' | 'short';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CursorFlowEvent<T = Record<string, any>> {
|
|
75
|
+
id: string;
|
|
76
|
+
type: string;
|
|
77
|
+
timestamp: string;
|
|
78
|
+
runId: string;
|
|
79
|
+
payload: T;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type EventHandler<T = any> = (event: CursorFlowEvent<T>) => void | Promise<void>;
|
|
83
|
+
|
|
84
|
+
// Specific Event Payloads
|
|
85
|
+
export interface OrchestrationStartedPayload {
|
|
86
|
+
runId: string;
|
|
87
|
+
tasksDir: string;
|
|
88
|
+
laneCount: number;
|
|
89
|
+
runRoot: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface OrchestrationCompletedPayload {
|
|
93
|
+
runId: string;
|
|
94
|
+
laneCount: number;
|
|
95
|
+
completedCount: number;
|
|
96
|
+
failedCount: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface OrchestrationFailedPayload {
|
|
100
|
+
error: string;
|
|
101
|
+
blockedLanes?: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface LaneStartedPayload {
|
|
105
|
+
laneName: string;
|
|
106
|
+
pid?: number;
|
|
107
|
+
logPath: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface LaneCompletedPayload {
|
|
111
|
+
laneName: string;
|
|
112
|
+
exitCode: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface LaneFailedPayload {
|
|
116
|
+
laneName: string;
|
|
117
|
+
exitCode: number;
|
|
118
|
+
error: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface LaneDependencyRequestedPayload {
|
|
122
|
+
laneName: string;
|
|
123
|
+
dependencyRequest: DependencyRequestPlan;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface TaskStartedPayload {
|
|
127
|
+
taskName: string;
|
|
128
|
+
taskBranch: string;
|
|
129
|
+
index: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface TaskCompletedPayload {
|
|
133
|
+
taskName: string;
|
|
134
|
+
taskBranch: string;
|
|
135
|
+
status: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TaskFailedPayload {
|
|
139
|
+
taskName: string;
|
|
140
|
+
taskBranch: string;
|
|
141
|
+
error: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface AgentPromptSentPayload {
|
|
145
|
+
taskName: string;
|
|
146
|
+
model: string;
|
|
147
|
+
promptLength: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface AgentResponseReceivedPayload {
|
|
151
|
+
taskName: string;
|
|
152
|
+
ok: boolean;
|
|
153
|
+
duration: number;
|
|
154
|
+
responseLength: number;
|
|
155
|
+
error?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ReviewStartedPayload {
|
|
159
|
+
taskName: string;
|
|
160
|
+
taskBranch: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ReviewCompletedPayload {
|
|
164
|
+
taskName: string;
|
|
165
|
+
status: 'approved' | 'needs_changes';
|
|
166
|
+
issueCount: number;
|
|
167
|
+
summary: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ReviewApprovedPayload {
|
|
171
|
+
taskName: string;
|
|
172
|
+
iterations: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ReviewRejectedPayload {
|
|
176
|
+
taskName: string;
|
|
177
|
+
reason: string;
|
|
178
|
+
iterations: number;
|
|
28
179
|
}
|
|
29
180
|
|
|
30
181
|
export interface DependencyPolicy {
|
|
@@ -49,10 +200,12 @@ export interface RunnerConfig {
|
|
|
49
200
|
baseBranch?: string;
|
|
50
201
|
model?: string;
|
|
51
202
|
dependencyPolicy: DependencyPolicy;
|
|
203
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
204
|
+
agentOutputFormat?: 'stream-json' | 'json' | 'plain';
|
|
52
205
|
reviewModel?: string;
|
|
53
206
|
maxReviewIterations?: number;
|
|
54
207
|
acceptanceCriteria?: string[];
|
|
55
|
-
/** Task execution timeout in milliseconds. Default:
|
|
208
|
+
/** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
|
|
56
209
|
timeout?: number;
|
|
57
210
|
/**
|
|
58
211
|
* Enable intervention feature (stdin piping for message injection).
|
|
@@ -60,6 +213,12 @@ export interface RunnerConfig {
|
|
|
60
213
|
* Default: false
|
|
61
214
|
*/
|
|
62
215
|
enableIntervention?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Disable Git operations (worktree, branch, push, commit).
|
|
218
|
+
* Useful for testing or environments without Git remote.
|
|
219
|
+
* Default: false
|
|
220
|
+
*/
|
|
221
|
+
noGit?: boolean;
|
|
63
222
|
}
|
|
64
223
|
|
|
65
224
|
export interface DependencyRequestPlan {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { events } from './events';
|
|
3
|
+
import { WebhookConfig, CursorFlowEvent } from './types';
|
|
4
|
+
import * as logger from './logger';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register webhooks from configuration
|
|
8
|
+
*/
|
|
9
|
+
export function registerWebhooks(configs: WebhookConfig[]) {
|
|
10
|
+
if (!configs || !Array.isArray(configs)) return;
|
|
11
|
+
|
|
12
|
+
for (const config of configs) {
|
|
13
|
+
if (config.enabled === false) continue;
|
|
14
|
+
|
|
15
|
+
const patterns = config.events || ['*'];
|
|
16
|
+
|
|
17
|
+
for (const pattern of patterns) {
|
|
18
|
+
events.on(pattern, async (event) => {
|
|
19
|
+
try {
|
|
20
|
+
await sendWebhook(config, event);
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
logger.error(`Webhook failed for ${config.url}: ${error.message}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send webhook with retry logic and HMAC signature
|
|
31
|
+
*/
|
|
32
|
+
async function sendWebhook(config: WebhookConfig, event: CursorFlowEvent) {
|
|
33
|
+
const payload = JSON.stringify(event);
|
|
34
|
+
const headers: Record<string, string> = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'User-Agent': 'CursorFlow-Orchestrator',
|
|
37
|
+
...config.headers,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Add HMAC signature if secret is provided
|
|
41
|
+
if (config.secret) {
|
|
42
|
+
const signature = crypto
|
|
43
|
+
.createHmac('sha256', config.secret)
|
|
44
|
+
.update(payload)
|
|
45
|
+
.digest('hex');
|
|
46
|
+
headers['X-CursorFlow-Signature'] = `sha256=${signature}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const retries = config.retries ?? 3;
|
|
50
|
+
const timeoutMs = config.timeoutMs ?? 10000;
|
|
51
|
+
|
|
52
|
+
let lastError: any;
|
|
53
|
+
|
|
54
|
+
for (let attempt = 1; attempt <= retries + 1; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
58
|
+
|
|
59
|
+
const response = await fetch(config.url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers,
|
|
62
|
+
body: payload,
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
clearTimeout(timeoutId);
|
|
67
|
+
|
|
68
|
+
if (response.ok) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
73
|
+
} catch (error: any) {
|
|
74
|
+
lastError = error;
|
|
75
|
+
|
|
76
|
+
if (attempt <= retries) {
|
|
77
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw lastError;
|
|
84
|
+
}
|
|
85
|
+
|