@litmers/cursorflow-orchestrator 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +83 -2
  3. package/commands/cursorflow-clean.md +20 -6
  4. package/commands/cursorflow-prepare.md +1 -1
  5. package/commands/cursorflow-resume.md +127 -6
  6. package/commands/cursorflow-run.md +2 -2
  7. package/commands/cursorflow-signal.md +11 -4
  8. package/dist/cli/clean.js +164 -12
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +6 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/logs.d.ts +8 -0
  14. package/dist/cli/logs.js +746 -0
  15. package/dist/cli/logs.js.map +1 -0
  16. package/dist/cli/monitor.js +113 -30
  17. package/dist/cli/monitor.js.map +1 -1
  18. package/dist/cli/prepare.js +1 -1
  19. package/dist/cli/resume.js +367 -18
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +2 -0
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/signal.js +34 -20
  24. package/dist/cli/signal.js.map +1 -1
  25. package/dist/core/orchestrator.d.ts +11 -1
  26. package/dist/core/orchestrator.js +257 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.js +20 -0
  29. package/dist/core/reviewer.js.map +1 -1
  30. package/dist/core/runner.js +113 -13
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +34 -0
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/enhanced-logger.d.ts +209 -0
  35. package/dist/utils/enhanced-logger.js +963 -0
  36. package/dist/utils/enhanced-logger.js.map +1 -0
  37. package/dist/utils/events.d.ts +59 -0
  38. package/dist/utils/events.js +37 -0
  39. package/dist/utils/events.js.map +1 -0
  40. package/dist/utils/git.d.ts +5 -0
  41. package/dist/utils/git.js +25 -0
  42. package/dist/utils/git.js.map +1 -1
  43. package/dist/utils/types.d.ts +122 -1
  44. package/dist/utils/webhook.d.ts +5 -0
  45. package/dist/utils/webhook.js +109 -0
  46. package/dist/utils/webhook.js.map +1 -0
  47. package/examples/README.md +1 -1
  48. package/package.json +1 -1
  49. package/scripts/simple-logging-test.sh +97 -0
  50. package/scripts/test-real-logging.sh +289 -0
  51. package/scripts/test-streaming-multi-task.sh +247 -0
  52. package/src/cli/clean.ts +170 -13
  53. package/src/cli/index.ts +4 -1
  54. package/src/cli/logs.ts +848 -0
  55. package/src/cli/monitor.ts +123 -30
  56. package/src/cli/prepare.ts +1 -1
  57. package/src/cli/resume.ts +463 -22
  58. package/src/cli/run.ts +2 -0
  59. package/src/cli/signal.ts +43 -27
  60. package/src/core/orchestrator.ts +303 -37
  61. package/src/core/reviewer.ts +22 -0
  62. package/src/core/runner.ts +128 -12
  63. package/src/utils/config.ts +36 -0
  64. package/src/utils/enhanced-logger.ts +1097 -0
  65. package/src/utils/events.ts +117 -0
  66. package/src/utils/git.ts +25 -0
  67. package/src/utils/types.ts +150 -1
  68. 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
@@ -323,3 +323,28 @@ export function getCommitInfo(commitHash: string, options: { cwd?: string } = {}
323
323
  subject: lines[5] || '',
324
324
  };
325
325
  }
326
+
327
+ /**
328
+ * Get diff statistics for the last operation (commit or merge)
329
+ * Comparing HEAD with its first parent
330
+ */
331
+ export function getLastOperationStats(cwd?: string): string {
332
+ try {
333
+ // Check if there are any commits
334
+ const hasCommits = runGitResult(['rev-parse', 'HEAD'], { cwd }).success;
335
+ if (!hasCommits) return '';
336
+
337
+ // Check if HEAD has a parent
338
+ const hasParent = runGitResult(['rev-parse', 'HEAD^1'], { cwd }).success;
339
+ if (!hasParent) {
340
+ // If no parent, show stats for the first commit
341
+ // Using an empty tree hash as the base
342
+ const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
343
+ return runGit(['diff', '--stat', emptyTree, 'HEAD'], { cwd, silent: true });
344
+ }
345
+
346
+ return runGit(['diff', '--stat', 'HEAD^1', 'HEAD'], { cwd, silent: true });
347
+ } catch (e) {
348
+ return '';
349
+ }
350
+ }
@@ -25,6 +25,155 @@ export interface CursorFlowConfig {
25
25
  worktreePrefix: string;
26
26
  maxConcurrentLanes: number;
27
27
  projectRoot: string;
28
+ webhooks?: WebhookConfig[];
29
+ /** Enhanced logging configuration */
30
+ enhancedLogging?: Partial<EnhancedLogConfig>;
31
+ }
32
+
33
+ export interface WebhookConfig {
34
+ enabled?: boolean;
35
+ url: string;
36
+ secret?: string;
37
+ events?: string[]; // ['*'] for all, ['task.*'] for wildcards
38
+ headers?: Record<string, string>;
39
+ retries?: number;
40
+ timeoutMs?: number;
41
+ }
42
+
43
+ /**
44
+ * Enhanced logging configuration
45
+ */
46
+ export interface EnhancedLogConfig {
47
+ /** Enable enhanced logging features (default: true) */
48
+ enabled: boolean;
49
+
50
+ /** Strip ANSI escape codes from clean logs (default: true) */
51
+ stripAnsi: boolean;
52
+
53
+ /** Add timestamps to each line (default: true) */
54
+ addTimestamps: boolean;
55
+
56
+ /** Maximum size in bytes before rotation (default: 50MB) */
57
+ maxFileSize: number;
58
+
59
+ /** Number of rotated files to keep (default: 5) */
60
+ maxFiles: number;
61
+
62
+ /** Write raw output with ANSI codes to separate file (default: true) */
63
+ keepRawLogs: boolean;
64
+
65
+ /** Write structured JSON log entries (default: true) */
66
+ writeJsonLog: boolean;
67
+
68
+ /** Timestamp format: 'iso' | 'relative' | 'short' (default: 'iso') */
69
+ timestampFormat: 'iso' | 'relative' | 'short';
70
+ }
71
+
72
+ export interface CursorFlowEvent<T = Record<string, any>> {
73
+ id: string;
74
+ type: string;
75
+ timestamp: string;
76
+ runId: string;
77
+ payload: T;
78
+ }
79
+
80
+ export type EventHandler<T = any> = (event: CursorFlowEvent<T>) => void | Promise<void>;
81
+
82
+ // Specific Event Payloads
83
+ export interface OrchestrationStartedPayload {
84
+ runId: string;
85
+ tasksDir: string;
86
+ laneCount: number;
87
+ runRoot: string;
88
+ }
89
+
90
+ export interface OrchestrationCompletedPayload {
91
+ runId: string;
92
+ laneCount: number;
93
+ completedCount: number;
94
+ failedCount: number;
95
+ }
96
+
97
+ export interface OrchestrationFailedPayload {
98
+ error: string;
99
+ blockedLanes?: string[];
100
+ }
101
+
102
+ export interface LaneStartedPayload {
103
+ laneName: string;
104
+ pid?: number;
105
+ logPath: string;
106
+ }
107
+
108
+ export interface LaneCompletedPayload {
109
+ laneName: string;
110
+ exitCode: number;
111
+ }
112
+
113
+ export interface LaneFailedPayload {
114
+ laneName: string;
115
+ exitCode: number;
116
+ error: string;
117
+ }
118
+
119
+ export interface LaneDependencyRequestedPayload {
120
+ laneName: string;
121
+ dependencyRequest: DependencyRequestPlan;
122
+ }
123
+
124
+ export interface TaskStartedPayload {
125
+ taskName: string;
126
+ taskBranch: string;
127
+ index: number;
128
+ }
129
+
130
+ export interface TaskCompletedPayload {
131
+ taskName: string;
132
+ taskBranch: string;
133
+ status: string;
134
+ }
135
+
136
+ export interface TaskFailedPayload {
137
+ taskName: string;
138
+ taskBranch: string;
139
+ error: string;
140
+ }
141
+
142
+ export interface AgentPromptSentPayload {
143
+ taskName: string;
144
+ model: string;
145
+ promptLength: number;
146
+ }
147
+
148
+ export interface AgentResponseReceivedPayload {
149
+ taskName: string;
150
+ ok: boolean;
151
+ duration: number;
152
+ responseLength: number;
153
+ error?: string;
154
+ }
155
+
156
+ export interface ReviewStartedPayload {
157
+ taskName: string;
158
+ taskBranch: string;
159
+ }
160
+
161
+ export interface ReviewCompletedPayload {
162
+ taskName: string;
163
+ status: 'approved' | 'needs_changes';
164
+ issueCount: number;
165
+ summary: string;
166
+ }
167
+
168
+ export interface ReviewApprovedPayload {
169
+ taskName: string;
170
+ iterations: number;
171
+ }
172
+
173
+ export interface ReviewRejectedPayload {
174
+ taskName: string;
175
+ reason: string;
176
+ iterations: number;
28
177
  }
29
178
 
30
179
  export interface DependencyPolicy {
@@ -52,7 +201,7 @@ export interface RunnerConfig {
52
201
  reviewModel?: string;
53
202
  maxReviewIterations?: number;
54
203
  acceptanceCriteria?: string[];
55
- /** Task execution timeout in milliseconds. Default: 300000 (5 minutes) */
204
+ /** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
56
205
  timeout?: number;
57
206
  /**
58
207
  * Enable intervention feature (stdin piping for message injection).
@@ -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
+