@goodandready/dsh-goal 0.2.9 → 0.2.11
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/lib/client.js +350 -13
- package/lib/engine-git-branch.js +105 -0
- package/lib/engine-intervention.js +150 -0
- package/lib/engine-issue-sync.js +194 -0
- package/lib/engine-milestones.js +42 -3
- package/lib/engine-prompt.js +28 -4
- package/lib/engine-retrospective.js +79 -0
- package/lib/engine-sessions.js +82 -0
- package/lib/engine-templates.js +167 -0
- package/lib/goal-engine-constants.js +30 -0
- package/lib/goal-engine.js +212 -236
- package/lib/index.js +54 -27
- package/lib/routes.js +97 -7
- package/lib/tools.js +19 -2
- package/package.json +1 -1
package/lib/goal-engine.js
CHANGED
|
@@ -3,6 +3,12 @@ import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, create
|
|
|
3
3
|
import { EngineStore } from './engine-store.js';
|
|
4
4
|
import { buildStatePromptInjection } from './engine-prompt.js';
|
|
5
5
|
import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
|
|
6
|
+
import { applyIntervention, recordActivity, processTokenBudget } from './engine-intervention.js';
|
|
7
|
+
import { createGoalBranch, sanitizeBranchSlug, getCurrentGitBranch, mergeGoalBranch, discardGoalBranch } from './engine-git-branch.js';
|
|
8
|
+
import { parseIssueRef, syncMilestoneWithIssue } from './engine-issue-sync.js';
|
|
9
|
+
import { generateRetrospectiveData } from './engine-retrospective.js';
|
|
10
|
+
import { generatePreplanDraft } from './engine-templates.js';
|
|
11
|
+
import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget } from './engine-sessions.js';
|
|
6
12
|
|
|
7
13
|
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone };
|
|
8
14
|
export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
@@ -20,6 +26,15 @@ export class GoalEngine {
|
|
|
20
26
|
this.maxTokenBudget = options.maxTokenBudget ?? 0;
|
|
21
27
|
this.budgetWarningThreshold = options.budgetWarningThreshold ?? 80;
|
|
22
28
|
this.autoCheckpointOnMilestone = options.autoCheckpointOnMilestone ?? false;
|
|
29
|
+
this.enableLiveActivityFeed = options.enableLiveActivityFeed ?? true;
|
|
30
|
+
this.enablePreplanning = options.enablePreplanning ?? true;
|
|
31
|
+
this.autoBranchOnGoalStart = options.autoBranchOnGoalStart ?? true;
|
|
32
|
+
this.enablePostGoalRetrospective = options.enablePostGoalRetrospective ?? true;
|
|
33
|
+
this.autoScaleBudgetNearCompletion = options.autoScaleBudgetNearCompletion ?? false;
|
|
34
|
+
this.enableTemplatesDrawer = options.enableTemplatesDrawer ?? true;
|
|
35
|
+
this.enableMilestoneDependencies = options.enableMilestoneDependencies ?? true;
|
|
36
|
+
this.soundScheme = options.soundScheme ?? 'default';
|
|
37
|
+
this.enableVoiceAnnouncements = options.enableVoiceAnnouncements ?? false;
|
|
23
38
|
this.maxSessions = options.maxSessions ?? 100;
|
|
24
39
|
this.goals = new Map();
|
|
25
40
|
this.listeners = new Set();
|
|
@@ -58,14 +73,13 @@ export class GoalEngine {
|
|
|
58
73
|
try {
|
|
59
74
|
listener(snapshot, sid);
|
|
60
75
|
} catch (err) {
|
|
61
|
-
console.error
|
|
76
|
+
console.error('[dsh-goal] Error in GoalEngine listener:', err);
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
recordProgress(sessionId = 'default') {
|
|
67
|
-
|
|
68
|
-
this.stallCounters.set(sid, 0);
|
|
82
|
+
this.stallCounters.set(sessionId || 'default', 0);
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
incrementStallCount(sessionId = 'default') {
|
|
@@ -77,8 +91,7 @@ export class GoalEngine {
|
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
getStallCount(sessionId = 'default') {
|
|
80
|
-
|
|
81
|
-
return this.stallCounters.get(sid) || 0;
|
|
94
|
+
return this.stallCounters.get(sessionId || 'default') || 0;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
updateConfig(config = {}) {
|
|
@@ -86,32 +99,25 @@ export class GoalEngine {
|
|
|
86
99
|
const prev = this.defaultMaxIterations;
|
|
87
100
|
this.defaultMaxIterations = config.defaultMaxIterations;
|
|
88
101
|
for (const [_, goal] of this.goals) {
|
|
89
|
-
if (goal && goal.maxIterations === prev)
|
|
90
|
-
goal.maxIterations = config.defaultMaxIterations;
|
|
91
|
-
}
|
|
102
|
+
if (goal && goal.maxIterations === prev) goal.maxIterations = config.defaultMaxIterations;
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
|
-
if (typeof config.autoDrive === 'boolean')
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (typeof config.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (typeof config.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (typeof config.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (typeof config.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (typeof config.
|
|
110
|
-
this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
|
|
111
|
-
}
|
|
112
|
-
if (typeof config.autoCheckpointOnMilestone === 'boolean') {
|
|
113
|
-
this.autoCheckpointOnMilestone = config.autoCheckpointOnMilestone;
|
|
114
|
-
}
|
|
105
|
+
if (typeof config.autoDrive === 'boolean') this.autoDrive = config.autoDrive;
|
|
106
|
+
if (typeof config.enableSound === 'boolean') this.enableSound = config.enableSound;
|
|
107
|
+
if (typeof config.showQuickLaunchButton === 'boolean') this.showQuickLaunchButton = config.showQuickLaunchButton;
|
|
108
|
+
if (typeof config.consecutiveToolFailureLimit === 'number') this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
|
|
109
|
+
if (typeof config.maxTokenBudget === 'number') this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
|
|
110
|
+
if (typeof config.budgetWarningThreshold === 'number') this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
|
|
111
|
+
if (typeof config.autoCheckpointOnMilestone === 'boolean') this.autoCheckpointOnMilestone = config.autoCheckpointOnMilestone;
|
|
112
|
+
if (typeof config.enableLiveActivityFeed === 'boolean') this.enableLiveActivityFeed = config.enableLiveActivityFeed;
|
|
113
|
+
if (typeof config.enablePreplanning === 'boolean') this.enablePreplanning = config.enablePreplanning;
|
|
114
|
+
if (typeof config.autoBranchOnGoalStart === 'boolean') this.autoBranchOnGoalStart = config.autoBranchOnGoalStart;
|
|
115
|
+
if (typeof config.enablePostGoalRetrospective === 'boolean') this.enablePostGoalRetrospective = config.enablePostGoalRetrospective;
|
|
116
|
+
if (typeof config.autoScaleBudgetNearCompletion === 'boolean') this.autoScaleBudgetNearCompletion = config.autoScaleBudgetNearCompletion;
|
|
117
|
+
if (typeof config.enableTemplatesDrawer === 'boolean') this.enableTemplatesDrawer = config.enableTemplatesDrawer;
|
|
118
|
+
if (typeof config.enableMilestoneDependencies === 'boolean') this.enableMilestoneDependencies = config.enableMilestoneDependencies;
|
|
119
|
+
if (typeof config.soundScheme === 'string') this.soundScheme = config.soundScheme;
|
|
120
|
+
if (typeof config.enableVoiceAnnouncements === 'boolean') this.enableVoiceAnnouncements = config.enableVoiceAnnouncements;
|
|
115
121
|
this.emit();
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -121,75 +127,39 @@ export class GoalEngine {
|
|
|
121
127
|
|
|
122
128
|
incrementToolFailureCount(sessionId = 'default') {
|
|
123
129
|
const sid = sessionId || 'default';
|
|
124
|
-
const
|
|
125
|
-
const next = current + 1;
|
|
130
|
+
const next = (this.toolFailureCounters.get(sid) || 0) + 1;
|
|
126
131
|
this.toolFailureCounters.set(sid, next);
|
|
127
132
|
return next;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
resetToolFailureCount(sessionId = 'default') {
|
|
131
|
-
|
|
132
|
-
this.toolFailureCounters.set(sid, 0);
|
|
136
|
+
this.toolFailureCounters.set(sessionId || 'default', 0);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
getToolFailureCount(sessionId = 'default') {
|
|
136
|
-
|
|
137
|
-
return this.toolFailureCounters.get(sid) || 0;
|
|
140
|
+
return this.toolFailureCounters.get(sessionId || 'default') || 0;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
nudge(text, sessionId = 'default') {
|
|
141
144
|
const sid = sessionId || 'default';
|
|
142
145
|
const goal = this.goals.get(sid);
|
|
143
146
|
if (!goal) return this.getSnapshot(sid);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!clean) return this.getSnapshot(sid);
|
|
147
|
-
|
|
148
|
-
if (!Array.isArray(goal.nudges)) {
|
|
149
|
-
goal.nudges = [];
|
|
150
|
-
}
|
|
151
|
-
const now = Date.now();
|
|
152
|
-
goal.nudges.push({ text: clean, timestamp: now });
|
|
153
|
-
goal.pendingNudge = clean;
|
|
154
|
-
goal.logs.push({
|
|
155
|
-
timestamp: now,
|
|
156
|
-
type: 'info',
|
|
157
|
-
message: `User steering / clarification: "${clean}"`,
|
|
158
|
-
});
|
|
159
|
-
if (goal.logs.length > 100) {
|
|
160
|
-
goal.logs = goal.logs.slice(-100);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
this.emit(sid, true);
|
|
147
|
+
const res = addNudgeToGoal(goal, text);
|
|
148
|
+
if (res) this.emit(sid, true);
|
|
164
149
|
return this.getSnapshot(sid);
|
|
165
150
|
}
|
|
166
151
|
|
|
167
152
|
consumePendingNudge(sessionId = 'default') {
|
|
168
153
|
const sid = sessionId || 'default';
|
|
169
154
|
const goal = this.goals.get(sid);
|
|
170
|
-
if (!goal
|
|
171
|
-
const nudge = goal
|
|
172
|
-
|
|
173
|
-
this.emit(sid);
|
|
155
|
+
if (!goal) return null;
|
|
156
|
+
const nudge = consumeGoalNudge(goal);
|
|
157
|
+
if (nudge) this.emit(sid);
|
|
174
158
|
return nudge;
|
|
175
159
|
}
|
|
176
160
|
|
|
177
161
|
pruneInactiveSessions() {
|
|
178
|
-
|
|
179
|
-
const inactive = [];
|
|
180
|
-
for (const [sid, goal] of this.goals.entries()) {
|
|
181
|
-
if (sid === 'default') continue;
|
|
182
|
-
if (goal.state === GoalState.COMPLETED || goal.state === GoalState.CANCELLED || goal.state === GoalState.FAILED) {
|
|
183
|
-
inactive.push({ sid, completedAt: goal.completedAt || goal.startedAt || 0 });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
inactive.sort((a, b) => a.completedAt - b.completedAt);
|
|
187
|
-
while (this.goals.size >= this.maxSessions && inactive.length > 0) {
|
|
188
|
-
const oldest = inactive.shift();
|
|
189
|
-
this.goals.delete(oldest.sid);
|
|
190
|
-
this.stallCounters.delete(oldest.sid);
|
|
191
|
-
this.toolFailureCounters.delete(oldest.sid);
|
|
192
|
-
}
|
|
162
|
+
pruneInactiveSessions(this.goals, this.stallCounters, this.toolFailureCounters, this.maxSessions);
|
|
193
163
|
}
|
|
194
164
|
|
|
195
165
|
clearSession(sessionId) {
|
|
@@ -207,18 +177,16 @@ export class GoalEngine {
|
|
|
207
177
|
}
|
|
208
178
|
|
|
209
179
|
this.pruneInactiveSessions();
|
|
210
|
-
|
|
211
180
|
const cleanTitle = title.trim();
|
|
212
181
|
const now = Date.now();
|
|
213
182
|
const sid = sessionId || 'default';
|
|
214
183
|
this.stallCounters.set(sid, 0);
|
|
184
|
+
this.toolFailureCounters.set(sid, 0);
|
|
215
185
|
|
|
216
186
|
const gitCommit = options.gitStartCommit !== undefined
|
|
217
187
|
? options.gitStartCommit
|
|
218
188
|
: getGitCurrentCommit(options.cwd);
|
|
219
189
|
|
|
220
|
-
this.toolFailureCounters.set(sid, 0);
|
|
221
|
-
|
|
222
190
|
const goal = {
|
|
223
191
|
id: `goal-${now}-${Math.random().toString(36).substring(2, 7)}`,
|
|
224
192
|
sessionId: sid,
|
|
@@ -235,21 +203,41 @@ export class GoalEngine {
|
|
|
235
203
|
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
236
204
|
maxTokenBudget: options.maxTokenBudget ?? this.maxTokenBudget,
|
|
237
205
|
budgetWarningTriggered: false,
|
|
206
|
+
budgetAutoScaled: false,
|
|
238
207
|
gitStartCommit: gitCommit || null,
|
|
208
|
+
gitBranch: null,
|
|
209
|
+
gitParentBranch: null,
|
|
239
210
|
pendingNudge: null,
|
|
240
211
|
nudges: [],
|
|
241
212
|
milestones: [],
|
|
213
|
+
activityFeed: [],
|
|
214
|
+
userDirective: null,
|
|
215
|
+
userDirectiveConsumed: false,
|
|
216
|
+
preplan: null,
|
|
217
|
+
retrospective: null,
|
|
218
|
+
issueRef: parseIssueRef(cleanTitle),
|
|
242
219
|
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
243
|
-
logs: [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
},
|
|
249
|
-
],
|
|
220
|
+
logs: [{
|
|
221
|
+
timestamp: now,
|
|
222
|
+
type: 'info',
|
|
223
|
+
message: `Goal initiated: "${cleanTitle}"` + (gitCommit ? ` (Git: ${gitCommit})` : ''),
|
|
224
|
+
}],
|
|
250
225
|
resultSummary: '',
|
|
251
226
|
};
|
|
252
227
|
|
|
228
|
+
if (this.autoBranchOnGoalStart && options.autoBranch !== false && this.storagePath !== null && process.env.NODE_ENV !== 'test') {
|
|
229
|
+
const slug = sanitizeBranchSlug(cleanTitle);
|
|
230
|
+
const parentBranch = getCurrentGitBranch(options.cwd);
|
|
231
|
+
if (createGoalBranch(options.cwd, slug)) {
|
|
232
|
+
goal.gitBranch = slug;
|
|
233
|
+
goal.gitParentBranch = parentBranch;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (this.enablePreplanning && options.preplan !== false && (!options.milestones || options.milestones.length === 0)) {
|
|
238
|
+
goal.preplan = { active: true, draftMilestones: generatePreplanDraft(cleanTitle) };
|
|
239
|
+
}
|
|
240
|
+
|
|
253
241
|
this.goals.set(sid, goal);
|
|
254
242
|
|
|
255
243
|
if (Array.isArray(options.milestones) && options.milestones.length > 0) {
|
|
@@ -263,22 +251,13 @@ export class GoalEngine {
|
|
|
263
251
|
pause(reason = 'User requested pause', sessionId = 'default') {
|
|
264
252
|
const sid = sessionId || 'default';
|
|
265
253
|
const goal = this.goals.get(sid);
|
|
266
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
267
|
-
return this.getSnapshot(sid);
|
|
268
|
-
}
|
|
254
|
+
if (!goal || goal.state !== GoalState.RUNNING) return this.getSnapshot(sid);
|
|
269
255
|
|
|
270
256
|
goal.state = GoalState.PAUSED;
|
|
271
257
|
goal.pausedAt = Date.now();
|
|
272
|
-
goal
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
message: `Paused: ${reason}`,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (goal.logs.length > 100) {
|
|
279
|
-
goal.logs = goal.logs.slice(-100);
|
|
280
|
-
}
|
|
281
|
-
|
|
258
|
+
recordActivity(goal, { type: 'control', tool: 'pause', status: 'warning', details: reason });
|
|
259
|
+
goal.logs.push({ timestamp: Date.now(), type: 'warning', message: `Paused: ${reason}` });
|
|
260
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
282
261
|
this.emit(sid, true);
|
|
283
262
|
return this.getSnapshot(sid);
|
|
284
263
|
}
|
|
@@ -286,29 +265,18 @@ export class GoalEngine {
|
|
|
286
265
|
resume(sessionId = 'default') {
|
|
287
266
|
const sid = sessionId || 'default';
|
|
288
267
|
const goal = this.goals.get(sid);
|
|
289
|
-
if (!goal || goal.state !== GoalState.PAUSED)
|
|
290
|
-
return this.getSnapshot(sid);
|
|
291
|
-
}
|
|
268
|
+
if (!goal || goal.state !== GoalState.PAUSED) return this.getSnapshot(sid);
|
|
292
269
|
|
|
293
270
|
const now = Date.now();
|
|
294
271
|
if (goal.pausedAt) {
|
|
295
272
|
goal.totalPausedDurationMs += now - goal.pausedAt;
|
|
296
273
|
goal.pausedAt = null;
|
|
297
274
|
}
|
|
298
|
-
|
|
299
275
|
goal.state = GoalState.RUNNING;
|
|
300
276
|
this.stallCounters.set(sid, 0);
|
|
301
|
-
|
|
302
|
-
goal.logs.push({
|
|
303
|
-
|
|
304
|
-
type: 'info',
|
|
305
|
-
message: 'Goal resumed',
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
if (goal.logs.length > 100) {
|
|
309
|
-
goal.logs = goal.logs.slice(-100);
|
|
310
|
-
}
|
|
311
|
-
|
|
277
|
+
recordActivity(goal, { type: 'control', tool: 'resume', status: 'info', details: 'Resumed by user' });
|
|
278
|
+
goal.logs.push({ timestamp: now, type: 'info', message: 'Goal resumed' });
|
|
279
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
312
280
|
this.emit(sid, true);
|
|
313
281
|
return this.getSnapshot(sid);
|
|
314
282
|
}
|
|
@@ -320,16 +288,9 @@ export class GoalEngine {
|
|
|
320
288
|
|
|
321
289
|
goal.state = GoalState.CANCELLED;
|
|
322
290
|
goal.completedAt = Date.now();
|
|
323
|
-
goal
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
message: `Cancelled: ${reason}`,
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
if (goal.logs.length > 100) {
|
|
330
|
-
goal.logs = goal.logs.slice(-100);
|
|
331
|
-
}
|
|
332
|
-
|
|
291
|
+
recordActivity(goal, { type: 'control', tool: 'cancel', status: 'warning', details: reason });
|
|
292
|
+
goal.logs.push({ timestamp: Date.now(), type: 'warning', message: `Cancelled: ${reason}` });
|
|
293
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
333
294
|
this.emit(sid, true);
|
|
334
295
|
return this.getSnapshot(sid);
|
|
335
296
|
}
|
|
@@ -352,38 +313,44 @@ export class GoalEngine {
|
|
|
352
313
|
goal.state = GoalState.COMPLETED;
|
|
353
314
|
goal.completedAt = now;
|
|
354
315
|
goal.resultSummary = summary;
|
|
316
|
+
if (this.enablePostGoalRetrospective) {
|
|
317
|
+
goal.retrospective = generateRetrospectiveData(goal, goal.cwd || process.cwd());
|
|
318
|
+
}
|
|
319
|
+
recordActivity(goal, { type: 'goal', tool: 'completeGoal', status: 'success', details: summary || 'Completed' });
|
|
355
320
|
goal.logs.push({
|
|
356
321
|
timestamp: now,
|
|
357
322
|
type: 'info',
|
|
358
323
|
message: `Goal completed successfully: ${summary || 'All objectives met.'}`,
|
|
359
324
|
});
|
|
360
|
-
|
|
361
|
-
if (goal.logs.length > 100) {
|
|
362
|
-
goal.logs = goal.logs.slice(-100);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
325
|
for (const m of goal.milestones) {
|
|
366
326
|
if (m.status === MilestoneStatus.IN_PROGRESS || m.status === MilestoneStatus.PENDING) {
|
|
367
327
|
m.status = MilestoneStatus.COMPLETED;
|
|
368
328
|
}
|
|
369
329
|
}
|
|
370
|
-
|
|
371
330
|
this.stallCounters.delete(sid);
|
|
372
331
|
this.toolFailureCounters.delete(sid);
|
|
373
332
|
this.emit(sid, true);
|
|
374
333
|
return this.getSnapshot(sid);
|
|
375
334
|
}
|
|
376
335
|
|
|
336
|
+
setMilestones(milestonesList, sessionId = 'default') {
|
|
337
|
+
this.addMilestones(milestonesList, true, sessionId);
|
|
338
|
+
return this.getSnapshot(sessionId);
|
|
339
|
+
}
|
|
340
|
+
|
|
377
341
|
addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
|
|
378
342
|
const sid = sessionId || 'default';
|
|
379
343
|
const goal = this.goals.get(sid);
|
|
380
|
-
if (!goal || !Array.isArray(milestonesList)) return;
|
|
381
|
-
|
|
382
|
-
const newItems = parseMilestoneItems(milestonesList, goal.milestones.length);
|
|
383
|
-
goal.milestones.push(...newItems);
|
|
344
|
+
if (!goal || !Array.isArray(milestonesList)) return [];
|
|
384
345
|
|
|
346
|
+
const newMilestones = parseMilestoneItems(milestonesList, goal.milestones.length);
|
|
347
|
+
for (const item of newMilestones) {
|
|
348
|
+
goal.milestones.push(item);
|
|
349
|
+
}
|
|
385
350
|
this.recordProgress(sid);
|
|
351
|
+
recordActivity(goal, { type: 'milestone', tool: 'addMilestones', status: 'success', details: `Added ${newMilestones.length} milestones` });
|
|
386
352
|
if (shouldEmit) this.emit(sid);
|
|
353
|
+
return newMilestones;
|
|
387
354
|
}
|
|
388
355
|
|
|
389
356
|
updateMilestone(id, status, notes = '', sessionId = 'default', checklist = null) {
|
|
@@ -394,37 +361,44 @@ export class GoalEngine {
|
|
|
394
361
|
const target = goal.milestones.find((m) => matchMilestone(m, id));
|
|
395
362
|
if (!target) return false;
|
|
396
363
|
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
target.checkpointCommit = commitHash;
|
|
410
|
-
goal.logs.push({
|
|
411
|
-
timestamp: Date.now(),
|
|
412
|
-
type: 'info',
|
|
413
|
-
message: `Git checkpoint saved for [${target.title}]: ${commitHash}`,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
364
|
+
const res = applyMilestoneUpdate(target, status, notes, checklist, Object.values(MilestoneStatus), {
|
|
365
|
+
enforceDependencies: this.enableMilestoneDependencies,
|
|
366
|
+
allMilestones: goal.milestones,
|
|
367
|
+
});
|
|
368
|
+
if (res.blocked) {
|
|
369
|
+
recordActivity(goal, {
|
|
370
|
+
type: 'milestone',
|
|
371
|
+
tool: 'updateMilestone',
|
|
372
|
+
status: 'error',
|
|
373
|
+
details: `Milestone ${id} blocked by ${res.missingDependencies?.join(', ')}`,
|
|
374
|
+
});
|
|
375
|
+
return false;
|
|
416
376
|
}
|
|
417
377
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
type: 'milestone',
|
|
421
|
-
message: `Milestone [${target.title}] status -> ${target.status}`,
|
|
422
|
-
});
|
|
378
|
+
const prevStatus = res.prevStatus;
|
|
379
|
+
recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'success', details: `${id} -> ${status}` });
|
|
423
380
|
|
|
424
|
-
if (
|
|
425
|
-
|
|
381
|
+
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED) {
|
|
382
|
+
if (this.autoCheckpointOnMilestone) {
|
|
383
|
+
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
384
|
+
if (commitHash) {
|
|
385
|
+
target.checkpointCommit = commitHash;
|
|
386
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Git checkpoint saved for [${target.title}]: ${commitHash}` });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (goal.issueRef) {
|
|
390
|
+
syncMilestoneWithIssue(goal, target).then((syncRes) => {
|
|
391
|
+
if (syncRes?.updated) {
|
|
392
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Synced checklist for issue #${syncRes.issueNumber}` });
|
|
393
|
+
recordActivity(goal, { type: 'issue', tool: 'syncIssueChecklist', status: 'success', details: `Issue #${syncRes.issueNumber} updated` });
|
|
394
|
+
this.emit(sid);
|
|
395
|
+
}
|
|
396
|
+
}).catch(() => {});
|
|
397
|
+
}
|
|
426
398
|
}
|
|
427
399
|
|
|
400
|
+
goal.logs.push({ timestamp: Date.now(), type: 'milestone', message: `Milestone [${target.title}] status -> ${target.status}` });
|
|
401
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
428
402
|
this.recordProgress(sid);
|
|
429
403
|
this.emit(sid);
|
|
430
404
|
return true;
|
|
@@ -434,40 +408,22 @@ export class GoalEngine {
|
|
|
434
408
|
const sid = sessionId || 'default';
|
|
435
409
|
const goal = this.goals.get(sid);
|
|
436
410
|
if (!goal) return false;
|
|
437
|
-
|
|
438
411
|
const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
|
|
412
|
+
if (!target) return false;
|
|
439
413
|
const ok = toggleMilestoneChecklistItem(target, itemIndex, done);
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return
|
|
414
|
+
if (ok) {
|
|
415
|
+
this.recordProgress(sid);
|
|
416
|
+
this.emit(sid);
|
|
417
|
+
}
|
|
418
|
+
return ok;
|
|
445
419
|
}
|
|
446
420
|
|
|
447
421
|
extendBudget(addTokens = 50000, sessionId = 'default') {
|
|
448
422
|
const sid = sessionId || 'default';
|
|
449
423
|
const goal = this.goals.get(sid);
|
|
450
424
|
if (!goal) return false;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const currentMax = goal.maxTokenBudget || this.maxTokenBudget || 0;
|
|
454
|
-
const newBudget = Math.max(currentTotal, currentMax) + Math.max(1000, Number(addTokens) || 50000);
|
|
455
|
-
|
|
456
|
-
goal.maxTokenBudget = newBudget;
|
|
457
|
-
goal.budgetWarningTriggered = false;
|
|
458
|
-
|
|
459
|
-
if (goal.state === GoalState.PAUSED && goal.logs?.some((l) => l.message?.includes('Token budget reached'))) {
|
|
460
|
-
goal.state = GoalState.RUNNING;
|
|
461
|
-
goal.pausedAt = null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
goal.logs.push({
|
|
465
|
-
timestamp: Date.now(),
|
|
466
|
-
type: 'info',
|
|
467
|
-
message: `Token budget extended to ${newBudget.toLocaleString('en-US')} tokens`,
|
|
468
|
-
});
|
|
469
|
-
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
470
|
-
|
|
425
|
+
extendGoalBudget(goal, addTokens, this.maxTokenBudget);
|
|
426
|
+
recordActivity(goal, { type: 'budget', tool: 'extendBudget', status: 'info', details: `+${addTokens} tokens` });
|
|
471
427
|
this.emit(sid, true);
|
|
472
428
|
return true;
|
|
473
429
|
}
|
|
@@ -475,22 +431,20 @@ export class GoalEngine {
|
|
|
475
431
|
incrementIteration(sessionId = 'default') {
|
|
476
432
|
const sid = sessionId || 'default';
|
|
477
433
|
const goal = this.goals.get(sid);
|
|
478
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
434
|
+
if (!goal || goal.state !== GoalState.RUNNING) return false;
|
|
481
435
|
|
|
482
436
|
goal.iterationsCount += 1;
|
|
483
|
-
|
|
484
437
|
if (goal.iterationsCount >= goal.maxIterations) {
|
|
485
438
|
goal.state = GoalState.FAILED;
|
|
439
|
+
goal.failedAt = Date.now();
|
|
440
|
+
goal.failureReason = "Max iterations (" + goal.maxIterations + ") reached";
|
|
441
|
+
recordActivity(goal, { type: 'control', tool: 'incrementIteration', status: 'error', details: goal.failureReason });
|
|
486
442
|
goal.logs.push({
|
|
487
443
|
timestamp: Date.now(),
|
|
488
|
-
type: '
|
|
489
|
-
message: `
|
|
444
|
+
type: 'warning',
|
|
445
|
+
message: `Max iterations (${goal.maxIterations}) reached. Pausing autonomous loop.`,
|
|
490
446
|
});
|
|
491
|
-
if (goal.logs.length > 100)
|
|
492
|
-
goal.logs = goal.logs.slice(-100);
|
|
493
|
-
}
|
|
447
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
494
448
|
this.emit(sid, true);
|
|
495
449
|
return false;
|
|
496
450
|
}
|
|
@@ -509,10 +463,7 @@ export class GoalEngine {
|
|
|
509
463
|
const goal = this.goals.get(sid);
|
|
510
464
|
if (!goal) return;
|
|
511
465
|
|
|
512
|
-
if (!goal.tokensUsage) {
|
|
513
|
-
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
514
|
-
}
|
|
515
|
-
|
|
466
|
+
if (!goal.tokensUsage) goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
516
467
|
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
517
468
|
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
518
469
|
const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
|
|
@@ -521,43 +472,22 @@ export class GoalEngine {
|
|
|
521
472
|
goal.tokensUsage.completionTokens += completion;
|
|
522
473
|
goal.tokensUsage.totalTokens += total;
|
|
523
474
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
timestamp: Date.now(),
|
|
534
|
-
type: 'warning',
|
|
535
|
-
message: `Token budget warning: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens consumed (${Math.round((currentTotal / budget) * 100)}%)`,
|
|
536
|
-
});
|
|
537
|
-
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (currentTotal >= budget && goal.state === GoalState.RUNNING) {
|
|
541
|
-
goal.state = GoalState.PAUSED;
|
|
542
|
-
goal.pausedAt = Date.now();
|
|
543
|
-
goal.logs.push({
|
|
544
|
-
timestamp: Date.now(),
|
|
545
|
-
type: 'warning',
|
|
546
|
-
message: `Token budget reached: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens. Pausing autonomous loop.`,
|
|
547
|
-
});
|
|
548
|
-
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
549
|
-
this.emit(sid, true);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
475
|
+
const budgetCheck = processTokenBudget(goal, {
|
|
476
|
+
maxTokenBudget: this.maxTokenBudget,
|
|
477
|
+
budgetWarningThreshold: this.budgetWarningThreshold,
|
|
478
|
+
autoScaleBudgetNearCompletion: this.autoScaleBudgetNearCompletion,
|
|
479
|
+
GoalState,
|
|
480
|
+
});
|
|
481
|
+
if (budgetCheck.paused) {
|
|
482
|
+
this.emit(sid, true);
|
|
483
|
+
return;
|
|
552
484
|
}
|
|
553
|
-
|
|
554
485
|
this.emit(sid);
|
|
555
486
|
}
|
|
556
487
|
|
|
557
488
|
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
558
489
|
const sid = sessionId || 'default';
|
|
559
|
-
|
|
560
|
-
return calculateRemainingSeconds(goal, this.getElapsedSeconds(sid));
|
|
490
|
+
return calculateRemainingSeconds(this.goals.get(sid), this.getElapsedSeconds(sid));
|
|
561
491
|
}
|
|
562
492
|
|
|
563
493
|
getSnapshot(sessionId = 'default') {
|
|
@@ -572,17 +502,63 @@ export class GoalEngine {
|
|
|
572
502
|
getStatePromptInjection(sessionId = 'default') {
|
|
573
503
|
const sid = sessionId || 'default';
|
|
574
504
|
const goal = this.goals.get(sid);
|
|
575
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
576
|
-
|
|
505
|
+
if (!goal || goal.state !== GoalState.RUNNING) return '';
|
|
506
|
+
|
|
507
|
+
const snap = this.getSnapshot(sid);
|
|
508
|
+
const nudge = this.consumePendingNudge(sid);
|
|
509
|
+
const injection = buildStatePromptInjection(snap, nudge);
|
|
510
|
+
if (goal.userDirective) goal.userDirectiveConsumed = true;
|
|
511
|
+
return injection;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
intervene(intervention = {}, sessionId = 'default') {
|
|
515
|
+
const sid = sessionId || 'default';
|
|
516
|
+
const goal = this.goals.get(sid);
|
|
517
|
+
if (!goal) return { ok: false, error: 'Goal not found' };
|
|
518
|
+
const res = applyIntervention(goal, intervention);
|
|
519
|
+
this.emit(sid, true);
|
|
520
|
+
return { ok: true, state: this.getSnapshot(sid) };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
approvePreplan(sessionId = 'default') {
|
|
524
|
+
const sid = sessionId || 'default';
|
|
525
|
+
const goal = this.goals.get(sid);
|
|
526
|
+
if (!goal || !goal.preplan?.active) return this.getSnapshot(sid);
|
|
527
|
+
const drafts = goal.preplan.draftMilestones || [];
|
|
528
|
+
goal.preplan.active = false;
|
|
529
|
+
if (drafts.length > 0 && goal.milestones.length === 0) {
|
|
530
|
+
this.addMilestones(drafts, false, sid);
|
|
577
531
|
}
|
|
532
|
+
this.emit(sid, true);
|
|
533
|
+
return this.getSnapshot(sid);
|
|
534
|
+
}
|
|
578
535
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
536
|
+
mergeGoalBranch(targetBranch = 'main', sessionId = 'default') {
|
|
537
|
+
const sid = sessionId || 'default';
|
|
538
|
+
const goal = this.goals.get(sid);
|
|
539
|
+
if (!goal || !goal.gitBranch) return false;
|
|
540
|
+
const ok = mergeGoalBranch(goal.cwd || process.cwd(), goal.gitBranch, targetBranch);
|
|
541
|
+
if (ok) recordActivity(goal, { type: 'git', tool: 'mergeGoalBranch', status: 'success', details: `Merged ${goal.gitBranch}` });
|
|
542
|
+
this.emit(sid, true);
|
|
543
|
+
return ok;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
discardGoalBranch(targetBranch = 'main', sessionId = 'default') {
|
|
547
|
+
const sid = sessionId || 'default';
|
|
548
|
+
const goal = this.goals.get(sid);
|
|
549
|
+
if (!goal || !goal.gitBranch) return false;
|
|
550
|
+
const ok = discardGoalBranch(goal.cwd || process.cwd(), goal.gitBranch, targetBranch);
|
|
551
|
+
if (ok) {
|
|
552
|
+
recordActivity(goal, { type: 'git', tool: 'discardGoalBranch', status: 'info', details: `Discarded ${goal.gitBranch}` });
|
|
553
|
+
goal.gitBranch = null;
|
|
584
554
|
}
|
|
555
|
+
this.emit(sid, true);
|
|
556
|
+
return ok;
|
|
557
|
+
}
|
|
585
558
|
|
|
586
|
-
|
|
559
|
+
recordToolActivity(tool, status, details, sessionId = 'default') {
|
|
560
|
+
const sid = sessionId || 'default';
|
|
561
|
+
const goal = this.goals.get(sid);
|
|
562
|
+
if (goal) recordActivity(goal, { type: 'tool', tool, status, details });
|
|
587
563
|
}
|
|
588
564
|
}
|