@flowdot.ai/daemon 1.0.0
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/LICENSE +45 -0
- package/README.md +51 -0
- package/dist/goals/DependencyResolver.d.ts +54 -0
- package/dist/goals/DependencyResolver.js +329 -0
- package/dist/goals/ErrorRecovery.d.ts +133 -0
- package/dist/goals/ErrorRecovery.js +489 -0
- package/dist/goals/GoalApiClient.d.ts +81 -0
- package/dist/goals/GoalApiClient.js +743 -0
- package/dist/goals/GoalCache.d.ts +65 -0
- package/dist/goals/GoalCache.js +243 -0
- package/dist/goals/GoalCommsHandler.d.ts +150 -0
- package/dist/goals/GoalCommsHandler.js +378 -0
- package/dist/goals/GoalExporter.d.ts +164 -0
- package/dist/goals/GoalExporter.js +318 -0
- package/dist/goals/GoalImporter.d.ts +107 -0
- package/dist/goals/GoalImporter.js +345 -0
- package/dist/goals/GoalManager.d.ts +110 -0
- package/dist/goals/GoalManager.js +535 -0
- package/dist/goals/GoalReporter.d.ts +105 -0
- package/dist/goals/GoalReporter.js +534 -0
- package/dist/goals/GoalScheduler.d.ts +102 -0
- package/dist/goals/GoalScheduler.js +209 -0
- package/dist/goals/GoalValidator.d.ts +72 -0
- package/dist/goals/GoalValidator.js +657 -0
- package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
- package/dist/goals/MetaGoalEnforcer.js +536 -0
- package/dist/goals/MilestoneBreaker.d.ts +74 -0
- package/dist/goals/MilestoneBreaker.js +348 -0
- package/dist/goals/PermissionBridge.d.ts +109 -0
- package/dist/goals/PermissionBridge.js +326 -0
- package/dist/goals/ProgressTracker.d.ts +113 -0
- package/dist/goals/ProgressTracker.js +324 -0
- package/dist/goals/ReviewScheduler.d.ts +106 -0
- package/dist/goals/ReviewScheduler.js +360 -0
- package/dist/goals/TaskExecutor.d.ts +116 -0
- package/dist/goals/TaskExecutor.js +370 -0
- package/dist/goals/TaskFeedback.d.ts +126 -0
- package/dist/goals/TaskFeedback.js +402 -0
- package/dist/goals/TaskGenerator.d.ts +75 -0
- package/dist/goals/TaskGenerator.js +329 -0
- package/dist/goals/TaskQueue.d.ts +84 -0
- package/dist/goals/TaskQueue.js +331 -0
- package/dist/goals/TaskSanitizer.d.ts +61 -0
- package/dist/goals/TaskSanitizer.js +464 -0
- package/dist/goals/errors.d.ts +116 -0
- package/dist/goals/errors.js +299 -0
- package/dist/goals/index.d.ts +24 -0
- package/dist/goals/index.js +23 -0
- package/dist/goals/types.d.ts +395 -0
- package/dist/goals/types.js +230 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/loop/DaemonIPC.d.ts +67 -0
- package/dist/loop/DaemonIPC.js +358 -0
- package/dist/loop/IntervalParser.d.ts +39 -0
- package/dist/loop/IntervalParser.js +217 -0
- package/dist/loop/LoopDaemon.d.ts +123 -0
- package/dist/loop/LoopDaemon.js +1821 -0
- package/dist/loop/LoopExecutor.d.ts +93 -0
- package/dist/loop/LoopExecutor.js +326 -0
- package/dist/loop/LoopManager.d.ts +79 -0
- package/dist/loop/LoopManager.js +476 -0
- package/dist/loop/LoopScheduler.d.ts +69 -0
- package/dist/loop/LoopScheduler.js +329 -0
- package/dist/loop/LoopStore.d.ts +57 -0
- package/dist/loop/LoopStore.js +406 -0
- package/dist/loop/LoopValidator.d.ts +55 -0
- package/dist/loop/LoopValidator.js +603 -0
- package/dist/loop/errors.d.ts +115 -0
- package/dist/loop/errors.js +312 -0
- package/dist/loop/index.d.ts +11 -0
- package/dist/loop/index.js +10 -0
- package/dist/loop/notifications/Notifier.d.ts +28 -0
- package/dist/loop/notifications/Notifier.js +78 -0
- package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
- package/dist/loop/notifications/SlackNotifier.js +203 -0
- package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
- package/dist/loop/notifications/TerminalNotifier.js +72 -0
- package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
- package/dist/loop/notifications/WebhookNotifier.js +123 -0
- package/dist/loop/notifications/index.d.ts +24 -0
- package/dist/loop/notifications/index.js +109 -0
- package/dist/loop/types.d.ts +280 -0
- package/dist/loop/types.js +222 -0
- package/package.json +92 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
const DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
3
|
+
const DEFAULT_STALLED_THRESHOLD = 7;
|
|
4
|
+
const DEFAULT_DEADLINE_WARNING = 3;
|
|
5
|
+
const DEFAULT_SNOOZE_HOURS = 24;
|
|
6
|
+
export class ReviewScheduler extends EventEmitter {
|
|
7
|
+
logger;
|
|
8
|
+
checkInterval;
|
|
9
|
+
stalledThresholdDays;
|
|
10
|
+
deadlineWarningDays;
|
|
11
|
+
enableAutoTriggers;
|
|
12
|
+
schedules = new Map();
|
|
13
|
+
reviews = new Map();
|
|
14
|
+
lastActivityDates = new Map();
|
|
15
|
+
checkTimer = null;
|
|
16
|
+
reviewCounter = 0;
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.logger = options.logger ?? console;
|
|
20
|
+
this.checkInterval = options.checkInterval ?? DEFAULT_CHECK_INTERVAL;
|
|
21
|
+
this.stalledThresholdDays =
|
|
22
|
+
options.stalledThresholdDays ?? DEFAULT_STALLED_THRESHOLD;
|
|
23
|
+
this.deadlineWarningDays =
|
|
24
|
+
options.deadlineWarningDays ?? DEFAULT_DEADLINE_WARNING;
|
|
25
|
+
this.enableAutoTriggers = options.enableAutoTriggers ?? true;
|
|
26
|
+
}
|
|
27
|
+
start() {
|
|
28
|
+
if (this.checkTimer)
|
|
29
|
+
return;
|
|
30
|
+
this.logger.info('REVIEW_SCHEDULER', 'Starting review scheduler');
|
|
31
|
+
this.checkTimer = setInterval(() => {
|
|
32
|
+
this.checkDueReviews();
|
|
33
|
+
}, this.checkInterval);
|
|
34
|
+
this.checkDueReviews();
|
|
35
|
+
}
|
|
36
|
+
stop() {
|
|
37
|
+
if (this.checkTimer) {
|
|
38
|
+
clearInterval(this.checkTimer);
|
|
39
|
+
this.checkTimer = null;
|
|
40
|
+
this.logger.info('REVIEW_SCHEDULER', 'Stopped review scheduler');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
setSchedule(config) {
|
|
44
|
+
this.schedules.set(config.goalHash, config);
|
|
45
|
+
if (config.enabled) {
|
|
46
|
+
const nextReview = this.calculateNextReviewDate(config);
|
|
47
|
+
this.scheduleReview(config.goalHash, 'progress', nextReview, config.frequency);
|
|
48
|
+
}
|
|
49
|
+
this.logger.info('REVIEW_SCHEDULER', `Set ${config.frequency} review schedule for goal ${config.goalHash}`);
|
|
50
|
+
}
|
|
51
|
+
removeSchedule(goalHash) {
|
|
52
|
+
this.schedules.delete(goalHash);
|
|
53
|
+
for (const [id, review] of this.reviews) {
|
|
54
|
+
if (review.goalHash === goalHash && !review.completedAt) {
|
|
55
|
+
this.reviews.delete(id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.logger.info('REVIEW_SCHEDULER', `Removed schedule for goal ${goalHash}`);
|
|
59
|
+
}
|
|
60
|
+
scheduleReview(goalHash, type, scheduledFor, frequency = 'custom', metadata) {
|
|
61
|
+
const review = {
|
|
62
|
+
id: `review_${++this.reviewCounter}_${Date.now()}`,
|
|
63
|
+
goalHash,
|
|
64
|
+
type,
|
|
65
|
+
scheduledFor,
|
|
66
|
+
frequency,
|
|
67
|
+
title: this.getReviewTitle(type, frequency),
|
|
68
|
+
description: this.getReviewDescription(type),
|
|
69
|
+
priority: this.getReviewPriority(type),
|
|
70
|
+
metadata,
|
|
71
|
+
createdAt: new Date(),
|
|
72
|
+
};
|
|
73
|
+
this.reviews.set(review.id, review);
|
|
74
|
+
this.emit('review:created', review);
|
|
75
|
+
this.logger.debug('REVIEW_SCHEDULER', `Scheduled ${type} review for ${goalHash} on ${scheduledFor.toISOString()}`);
|
|
76
|
+
return review;
|
|
77
|
+
}
|
|
78
|
+
completeReview(reviewId, result) {
|
|
79
|
+
const review = this.reviews.get(reviewId);
|
|
80
|
+
if (!review) {
|
|
81
|
+
this.logger.warn('REVIEW_SCHEDULER', `Review not found: ${reviewId}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const completed = {
|
|
85
|
+
...review,
|
|
86
|
+
completedAt: new Date(),
|
|
87
|
+
};
|
|
88
|
+
this.reviews.set(reviewId, completed);
|
|
89
|
+
this.emit('review:completed', completed, result);
|
|
90
|
+
if (review.frequency !== 'custom' && review.frequency !== 'milestone') {
|
|
91
|
+
const config = this.schedules.get(review.goalHash);
|
|
92
|
+
if (config?.enabled) {
|
|
93
|
+
const nextDate = this.calculateNextReviewDate(config);
|
|
94
|
+
this.scheduleReview(review.goalHash, review.type, nextDate, review.frequency);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
snoozeReview(reviewId, hours = DEFAULT_SNOOZE_HOURS) {
|
|
99
|
+
const review = this.reviews.get(reviewId);
|
|
100
|
+
if (!review) {
|
|
101
|
+
this.logger.warn('REVIEW_SCHEDULER', `Review not found: ${reviewId}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const snoozedUntil = new Date();
|
|
105
|
+
snoozedUntil.setHours(snoozedUntil.getHours() + hours);
|
|
106
|
+
const snoozed = {
|
|
107
|
+
...review,
|
|
108
|
+
snoozedUntil,
|
|
109
|
+
};
|
|
110
|
+
this.reviews.set(reviewId, snoozed);
|
|
111
|
+
this.emit('review:snoozed', snoozed, snoozedUntil);
|
|
112
|
+
this.logger.info('REVIEW_SCHEDULER', `Snoozed review ${reviewId} until ${snoozedUntil.toISOString()}`);
|
|
113
|
+
}
|
|
114
|
+
getPendingReviews(goalHash) {
|
|
115
|
+
const pending = [];
|
|
116
|
+
const now = new Date();
|
|
117
|
+
for (const review of this.reviews.values()) {
|
|
118
|
+
if (review.completedAt)
|
|
119
|
+
continue;
|
|
120
|
+
if (goalHash && review.goalHash !== goalHash)
|
|
121
|
+
continue;
|
|
122
|
+
if (review.snoozedUntil && review.snoozedUntil > now)
|
|
123
|
+
continue;
|
|
124
|
+
pending.push(review);
|
|
125
|
+
}
|
|
126
|
+
return pending.sort((a, b) => a.scheduledFor.getTime() - b.scheduledFor.getTime());
|
|
127
|
+
}
|
|
128
|
+
getDueReviews() {
|
|
129
|
+
const now = new Date();
|
|
130
|
+
return this.getPendingReviews().filter((r) => r.scheduledFor <= now);
|
|
131
|
+
}
|
|
132
|
+
getUpcomingReviews(withinHours = 24) {
|
|
133
|
+
const now = new Date();
|
|
134
|
+
const cutoff = new Date();
|
|
135
|
+
cutoff.setHours(cutoff.getHours() + withinHours);
|
|
136
|
+
return this.getPendingReviews().filter((r) => r.scheduledFor > now && r.scheduledFor <= cutoff);
|
|
137
|
+
}
|
|
138
|
+
recordActivity(goalHash) {
|
|
139
|
+
this.lastActivityDates.set(goalHash, new Date());
|
|
140
|
+
}
|
|
141
|
+
checkStalledGoals(goals) {
|
|
142
|
+
const stalled = [];
|
|
143
|
+
const now = new Date();
|
|
144
|
+
const thresholdMs = this.stalledThresholdDays * 24 * 60 * 60 * 1000;
|
|
145
|
+
for (const goal of goals) {
|
|
146
|
+
if (goal.status !== 'active')
|
|
147
|
+
continue;
|
|
148
|
+
const lastActivity = this.lastActivityDates.get(goal.hash);
|
|
149
|
+
if (!lastActivity) {
|
|
150
|
+
const createdAt = new Date(goal.createdAt);
|
|
151
|
+
if (now.getTime() - createdAt.getTime() > thresholdMs) {
|
|
152
|
+
stalled.push(goal.hash);
|
|
153
|
+
const days = Math.floor((now.getTime() - createdAt.getTime()) / (24 * 60 * 60 * 1000));
|
|
154
|
+
this.emit('goal:stalled', goal.hash, days);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (now.getTime() - lastActivity.getTime() > thresholdMs) {
|
|
158
|
+
stalled.push(goal.hash);
|
|
159
|
+
const days = Math.floor((now.getTime() - lastActivity.getTime()) / (24 * 60 * 60 * 1000));
|
|
160
|
+
this.emit('goal:stalled', goal.hash, days);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return stalled;
|
|
164
|
+
}
|
|
165
|
+
checkApproachingDeadlines(goals) {
|
|
166
|
+
const approaching = [];
|
|
167
|
+
const now = new Date();
|
|
168
|
+
const warningMs = this.deadlineWarningDays * 24 * 60 * 60 * 1000;
|
|
169
|
+
for (const goal of goals) {
|
|
170
|
+
if (!goal.deadline)
|
|
171
|
+
continue;
|
|
172
|
+
if (goal.status === 'completed' || goal.status === 'abandoned')
|
|
173
|
+
continue;
|
|
174
|
+
const deadline = new Date(goal.deadline);
|
|
175
|
+
const remaining = deadline.getTime() - now.getTime();
|
|
176
|
+
if (remaining > 0 && remaining <= warningMs) {
|
|
177
|
+
approaching.push(goal.hash);
|
|
178
|
+
const daysRemaining = Math.ceil(remaining / (24 * 60 * 60 * 1000));
|
|
179
|
+
this.emit('deadline:approaching', goal.hash, daysRemaining);
|
|
180
|
+
const existingReview = this.findReview(goal.hash, 'deadline');
|
|
181
|
+
if (!existingReview) {
|
|
182
|
+
this.scheduleReview(goal.hash, 'deadline', new Date(), 'custom', { daysRemaining });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return approaching;
|
|
187
|
+
}
|
|
188
|
+
triggerMilestoneReview(goalHash, milestoneTitle) {
|
|
189
|
+
return this.scheduleReview(goalHash, 'milestone', new Date(), 'milestone', { milestoneTitle });
|
|
190
|
+
}
|
|
191
|
+
triggerCompletionReview(goalHash) {
|
|
192
|
+
return this.scheduleReview(goalHash, 'completion', new Date(), 'custom');
|
|
193
|
+
}
|
|
194
|
+
getStatistics() {
|
|
195
|
+
let completed = 0;
|
|
196
|
+
let pending = 0;
|
|
197
|
+
let overdue = 0;
|
|
198
|
+
let snoozed = 0;
|
|
199
|
+
const now = new Date();
|
|
200
|
+
for (const review of this.reviews.values()) {
|
|
201
|
+
if (review.completedAt) {
|
|
202
|
+
completed++;
|
|
203
|
+
}
|
|
204
|
+
else if (review.snoozedUntil && review.snoozedUntil > now) {
|
|
205
|
+
snoozed++;
|
|
206
|
+
}
|
|
207
|
+
else if (review.scheduledFor <= now) {
|
|
208
|
+
overdue++;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
pending++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
totalScheduled: this.reviews.size,
|
|
216
|
+
completed,
|
|
217
|
+
pending,
|
|
218
|
+
overdue,
|
|
219
|
+
snoozed,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
clearGoalData(goalHash) {
|
|
223
|
+
this.schedules.delete(goalHash);
|
|
224
|
+
this.lastActivityDates.delete(goalHash);
|
|
225
|
+
for (const [id, review] of this.reviews) {
|
|
226
|
+
if (review.goalHash === goalHash) {
|
|
227
|
+
this.reviews.delete(id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
checkDueReviews() {
|
|
232
|
+
const now = new Date();
|
|
233
|
+
const dueReviews = this.getDueReviews();
|
|
234
|
+
for (const review of dueReviews) {
|
|
235
|
+
this.emit('review:due', review);
|
|
236
|
+
}
|
|
237
|
+
const upcoming = this.getUpcomingReviews(1);
|
|
238
|
+
for (const review of upcoming) {
|
|
239
|
+
const daysUntil = (review.scheduledFor.getTime() - now.getTime()) / (24 * 60 * 60 * 1000);
|
|
240
|
+
this.emit('review:upcoming', review, daysUntil);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
calculateNextReviewDate(config) {
|
|
244
|
+
const now = new Date();
|
|
245
|
+
const next = new Date();
|
|
246
|
+
let hours = 9;
|
|
247
|
+
let minutes = 0;
|
|
248
|
+
if (config.preferredTime) {
|
|
249
|
+
const [h, m] = config.preferredTime.split(':').map(Number);
|
|
250
|
+
hours = h;
|
|
251
|
+
minutes = m;
|
|
252
|
+
}
|
|
253
|
+
switch (config.frequency) {
|
|
254
|
+
case 'daily':
|
|
255
|
+
next.setDate(next.getDate() + 1);
|
|
256
|
+
break;
|
|
257
|
+
case 'weekly':
|
|
258
|
+
next.setDate(next.getDate() + 7);
|
|
259
|
+
if (config.preferredDay !== undefined) {
|
|
260
|
+
const currentDay = next.getDay();
|
|
261
|
+
const targetDay = config.preferredDay;
|
|
262
|
+
const daysToAdd = (targetDay - currentDay + 7) % 7;
|
|
263
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
case 'biweekly':
|
|
267
|
+
next.setDate(next.getDate() + 14);
|
|
268
|
+
break;
|
|
269
|
+
case 'monthly':
|
|
270
|
+
next.setMonth(next.getMonth() + 1);
|
|
271
|
+
if (config.preferredDay !== undefined) {
|
|
272
|
+
next.setDate(Math.min(config.preferredDay, this.getDaysInMonth(next)));
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
case 'custom':
|
|
276
|
+
if (config.customIntervalDays) {
|
|
277
|
+
next.setDate(next.getDate() + config.customIntervalDays);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
next.setDate(next.getDate() + 7);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
case 'milestone':
|
|
284
|
+
return now;
|
|
285
|
+
}
|
|
286
|
+
next.setHours(hours, minutes, 0, 0);
|
|
287
|
+
return next;
|
|
288
|
+
}
|
|
289
|
+
getDaysInMonth(date) {
|
|
290
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
|
|
291
|
+
}
|
|
292
|
+
findReview(goalHash, type) {
|
|
293
|
+
for (const review of this.reviews.values()) {
|
|
294
|
+
if (review.goalHash === goalHash &&
|
|
295
|
+
review.type === type &&
|
|
296
|
+
!review.completedAt) {
|
|
297
|
+
return review;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
getReviewTitle(type, frequency) {
|
|
303
|
+
switch (type) {
|
|
304
|
+
case 'progress':
|
|
305
|
+
return `${this.capitalizeFirst(frequency)} Progress Review`;
|
|
306
|
+
case 'milestone':
|
|
307
|
+
return 'Milestone Completion Review';
|
|
308
|
+
case 'stalled':
|
|
309
|
+
return 'Stalled Goal Check-in';
|
|
310
|
+
case 'deadline':
|
|
311
|
+
return 'Deadline Approaching Review';
|
|
312
|
+
case 'weekly_summary':
|
|
313
|
+
return 'Weekly Summary';
|
|
314
|
+
case 'monthly_summary':
|
|
315
|
+
return 'Monthly Summary';
|
|
316
|
+
case 'completion':
|
|
317
|
+
return 'Goal Completion Review';
|
|
318
|
+
default:
|
|
319
|
+
return 'Goal Review';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
getReviewDescription(type) {
|
|
323
|
+
switch (type) {
|
|
324
|
+
case 'progress':
|
|
325
|
+
return 'Review progress and adjust priorities';
|
|
326
|
+
case 'milestone':
|
|
327
|
+
return 'Celebrate milestone and plan next steps';
|
|
328
|
+
case 'stalled':
|
|
329
|
+
return 'Review blockers and decide next actions';
|
|
330
|
+
case 'deadline':
|
|
331
|
+
return 'Assess readiness for upcoming deadline';
|
|
332
|
+
case 'weekly_summary':
|
|
333
|
+
return 'Weekly progress summary and planning';
|
|
334
|
+
case 'monthly_summary':
|
|
335
|
+
return 'Monthly retrospective and goal assessment';
|
|
336
|
+
case 'completion':
|
|
337
|
+
return 'Celebrate completion and capture learnings';
|
|
338
|
+
default:
|
|
339
|
+
return 'Review goal status';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
getReviewPriority(type) {
|
|
343
|
+
switch (type) {
|
|
344
|
+
case 'deadline':
|
|
345
|
+
case 'stalled':
|
|
346
|
+
return 'high';
|
|
347
|
+
case 'milestone':
|
|
348
|
+
case 'completion':
|
|
349
|
+
return 'medium';
|
|
350
|
+
default:
|
|
351
|
+
return 'low';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
capitalizeFirst(str) {
|
|
355
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
export function createReviewScheduler(options) {
|
|
359
|
+
return new ReviewScheduler(options);
|
|
360
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Goal, GoalHash, Task, TaskStatus, TaskType, ApprovalMode, Logger } from './types.js';
|
|
3
|
+
import { GoalError } from './errors.js';
|
|
4
|
+
export type PermissionCategory = 'file-read' | 'file-write' | 'file-create' | 'command-execute' | 'web-search' | 'recipe-invoke' | 'domain-access' | 'loop-create';
|
|
5
|
+
export interface PermissionCheckResult {
|
|
6
|
+
readonly allowed: boolean;
|
|
7
|
+
readonly requiresApproval: boolean;
|
|
8
|
+
readonly reason?: string;
|
|
9
|
+
readonly approvalMode: ApprovalMode;
|
|
10
|
+
readonly category: PermissionCategory;
|
|
11
|
+
}
|
|
12
|
+
export interface ApprovalResult {
|
|
13
|
+
readonly approved: boolean;
|
|
14
|
+
readonly response: 'approve' | 'deny' | 'skip' | 'later' | 'timeout';
|
|
15
|
+
readonly userId?: number;
|
|
16
|
+
readonly respondedAt?: Date;
|
|
17
|
+
}
|
|
18
|
+
export interface ExecutionResult {
|
|
19
|
+
readonly success: boolean;
|
|
20
|
+
readonly taskId: number;
|
|
21
|
+
readonly goalHash: GoalHash;
|
|
22
|
+
readonly status: TaskStatus;
|
|
23
|
+
readonly data?: unknown;
|
|
24
|
+
readonly error?: string;
|
|
25
|
+
readonly durationMs: number;
|
|
26
|
+
readonly cost?: number;
|
|
27
|
+
readonly permissionDenied?: boolean;
|
|
28
|
+
readonly approvalSkipped?: boolean;
|
|
29
|
+
readonly executedAt: Date;
|
|
30
|
+
}
|
|
31
|
+
export interface ExecutionContext {
|
|
32
|
+
readonly goal: Goal;
|
|
33
|
+
readonly task: Task;
|
|
34
|
+
readonly permissionCategory: PermissionCategory;
|
|
35
|
+
readonly approvalMode: ApprovalMode;
|
|
36
|
+
readonly startTime: Date;
|
|
37
|
+
}
|
|
38
|
+
export type TaskExecutionHandler = (context: ExecutionContext) => Promise<{
|
|
39
|
+
success: boolean;
|
|
40
|
+
data?: unknown;
|
|
41
|
+
error?: string;
|
|
42
|
+
cost?: number;
|
|
43
|
+
}>;
|
|
44
|
+
export type PermissionCheckFunction = (category: PermissionCategory, context: ExecutionContext) => Promise<PermissionCheckResult>;
|
|
45
|
+
export type ApprovalRequestFunction = (context: ExecutionContext) => Promise<ApprovalResult>;
|
|
46
|
+
export type TaskStatusUpdateFunction = (taskId: number, status: TaskStatus, result?: {
|
|
47
|
+
data?: unknown;
|
|
48
|
+
error?: string;
|
|
49
|
+
cost?: number;
|
|
50
|
+
}) => Promise<void>;
|
|
51
|
+
export interface TaskExecutorOptions {
|
|
52
|
+
readonly logger?: Logger;
|
|
53
|
+
readonly checkPermission?: PermissionCheckFunction;
|
|
54
|
+
readonly requestApproval?: ApprovalRequestFunction;
|
|
55
|
+
readonly updateTaskStatus?: TaskStatusUpdateFunction;
|
|
56
|
+
readonly handlers?: Partial<Record<TaskType, TaskExecutionHandler>>;
|
|
57
|
+
readonly maxExecutionTimeMs?: number;
|
|
58
|
+
readonly logExecutions?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface TaskExecutorEvents {
|
|
61
|
+
'execution-started': [ExecutionContext];
|
|
62
|
+
'permission-checked': [PermissionCheckResult, ExecutionContext];
|
|
63
|
+
'approval-requested': [ExecutionContext];
|
|
64
|
+
'approval-received': [ApprovalResult, ExecutionContext];
|
|
65
|
+
'execution-completed': [ExecutionResult];
|
|
66
|
+
'execution-failed': [ExecutionResult];
|
|
67
|
+
'error': [Error];
|
|
68
|
+
}
|
|
69
|
+
export declare class TaskExecutionError extends GoalError {
|
|
70
|
+
readonly taskId: number;
|
|
71
|
+
readonly goalHash: GoalHash;
|
|
72
|
+
constructor(message: string, taskId: number, goalHash: GoalHash, cause?: Error);
|
|
73
|
+
}
|
|
74
|
+
export declare class PermissionDeniedError extends GoalError {
|
|
75
|
+
readonly category: PermissionCategory;
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
constructor(category: PermissionCategory, reason: string);
|
|
78
|
+
}
|
|
79
|
+
export declare class ExecutionTimeoutError extends GoalError {
|
|
80
|
+
readonly taskId: number;
|
|
81
|
+
readonly timeoutMs: number;
|
|
82
|
+
constructor(taskId: number, timeoutMs: number);
|
|
83
|
+
}
|
|
84
|
+
export declare class TaskExecutor extends EventEmitter<TaskExecutorEvents> {
|
|
85
|
+
private readonly logger;
|
|
86
|
+
private readonly checkPermission?;
|
|
87
|
+
private readonly requestApproval?;
|
|
88
|
+
private readonly updateTaskStatus?;
|
|
89
|
+
private readonly handlers;
|
|
90
|
+
private readonly maxExecutionTimeMs;
|
|
91
|
+
private readonly logExecutions;
|
|
92
|
+
private readonly executingTasks;
|
|
93
|
+
constructor(options?: TaskExecutorOptions);
|
|
94
|
+
execute(goal: Goal, task: Task): Promise<ExecutionResult>;
|
|
95
|
+
executeSequence(goal: Goal, tasks: Task[]): Promise<ExecutionResult[]>;
|
|
96
|
+
canExecute(goal: Goal, task: Task): Promise<{
|
|
97
|
+
canExecute: boolean;
|
|
98
|
+
requiresApproval: boolean;
|
|
99
|
+
reason?: string;
|
|
100
|
+
}>;
|
|
101
|
+
private checkTaskPermission;
|
|
102
|
+
private defaultPermissionCheck;
|
|
103
|
+
private requestTaskApproval;
|
|
104
|
+
private executeTask;
|
|
105
|
+
registerHandler(taskType: TaskType, handler: TaskExecutionHandler): void;
|
|
106
|
+
unregisterHandler(taskType: TaskType): void;
|
|
107
|
+
hasHandler(taskType: TaskType): boolean;
|
|
108
|
+
getPermissionCategory(taskType: TaskType): PermissionCategory;
|
|
109
|
+
private getStatusFromApprovalResponse;
|
|
110
|
+
private updateStatus;
|
|
111
|
+
private createResult;
|
|
112
|
+
getExecutingTasks(): ExecutionContext[];
|
|
113
|
+
isExecuting(taskId: number): boolean;
|
|
114
|
+
cancelTask(taskId: number): boolean;
|
|
115
|
+
}
|
|
116
|
+
export declare function createTaskExecutor(options?: TaskExecutorOptions): TaskExecutor;
|