@goodandready/dsh-goal 0.2.8 → 0.2.10
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 +66 -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 +199 -233
- package/lib/index.js +36 -0
- package/lib/routes.js +139 -9
- package/lib/tools.js +11 -1
- 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 } 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,24 +177,23 @@ 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,
|
|
225
193
|
title: cleanTitle,
|
|
226
194
|
description: options.description?.trim() || '',
|
|
227
195
|
lang: options.lang || detectLanguage(cleanTitle),
|
|
196
|
+
cwd: options.cwd ? String(options.cwd).trim() : null,
|
|
228
197
|
state: GoalState.RUNNING,
|
|
229
198
|
startedAt: now,
|
|
230
199
|
pausedAt: null,
|
|
@@ -234,21 +203,41 @@ export class GoalEngine {
|
|
|
234
203
|
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
235
204
|
maxTokenBudget: options.maxTokenBudget ?? this.maxTokenBudget,
|
|
236
205
|
budgetWarningTriggered: false,
|
|
206
|
+
budgetAutoScaled: false,
|
|
237
207
|
gitStartCommit: gitCommit || null,
|
|
208
|
+
gitBranch: null,
|
|
209
|
+
gitParentBranch: null,
|
|
238
210
|
pendingNudge: null,
|
|
239
211
|
nudges: [],
|
|
240
212
|
milestones: [],
|
|
213
|
+
activityFeed: [],
|
|
214
|
+
userDirective: null,
|
|
215
|
+
userDirectiveConsumed: false,
|
|
216
|
+
preplan: null,
|
|
217
|
+
retrospective: null,
|
|
218
|
+
issueRef: parseIssueRef(cleanTitle),
|
|
241
219
|
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
242
|
-
logs: [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
},
|
|
248
|
-
],
|
|
220
|
+
logs: [{
|
|
221
|
+
timestamp: now,
|
|
222
|
+
type: 'info',
|
|
223
|
+
message: `Goal initiated: "${cleanTitle}"` + (gitCommit ? ` (Git: ${gitCommit})` : ''),
|
|
224
|
+
}],
|
|
249
225
|
resultSummary: '',
|
|
250
226
|
};
|
|
251
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
|
+
|
|
252
241
|
this.goals.set(sid, goal);
|
|
253
242
|
|
|
254
243
|
if (Array.isArray(options.milestones) && options.milestones.length > 0) {
|
|
@@ -262,22 +251,13 @@ export class GoalEngine {
|
|
|
262
251
|
pause(reason = 'User requested pause', sessionId = 'default') {
|
|
263
252
|
const sid = sessionId || 'default';
|
|
264
253
|
const goal = this.goals.get(sid);
|
|
265
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
266
|
-
return this.getSnapshot(sid);
|
|
267
|
-
}
|
|
254
|
+
if (!goal || goal.state !== GoalState.RUNNING) return this.getSnapshot(sid);
|
|
268
255
|
|
|
269
256
|
goal.state = GoalState.PAUSED;
|
|
270
257
|
goal.pausedAt = Date.now();
|
|
271
|
-
goal
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
message: `Paused: ${reason}`,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
if (goal.logs.length > 100) {
|
|
278
|
-
goal.logs = goal.logs.slice(-100);
|
|
279
|
-
}
|
|
280
|
-
|
|
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);
|
|
281
261
|
this.emit(sid, true);
|
|
282
262
|
return this.getSnapshot(sid);
|
|
283
263
|
}
|
|
@@ -285,29 +265,18 @@ export class GoalEngine {
|
|
|
285
265
|
resume(sessionId = 'default') {
|
|
286
266
|
const sid = sessionId || 'default';
|
|
287
267
|
const goal = this.goals.get(sid);
|
|
288
|
-
if (!goal || goal.state !== GoalState.PAUSED)
|
|
289
|
-
return this.getSnapshot(sid);
|
|
290
|
-
}
|
|
268
|
+
if (!goal || goal.state !== GoalState.PAUSED) return this.getSnapshot(sid);
|
|
291
269
|
|
|
292
270
|
const now = Date.now();
|
|
293
271
|
if (goal.pausedAt) {
|
|
294
272
|
goal.totalPausedDurationMs += now - goal.pausedAt;
|
|
295
273
|
goal.pausedAt = null;
|
|
296
274
|
}
|
|
297
|
-
|
|
298
275
|
goal.state = GoalState.RUNNING;
|
|
299
276
|
this.stallCounters.set(sid, 0);
|
|
300
|
-
|
|
301
|
-
goal.logs.push({
|
|
302
|
-
|
|
303
|
-
type: 'info',
|
|
304
|
-
message: 'Goal resumed',
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
if (goal.logs.length > 100) {
|
|
308
|
-
goal.logs = goal.logs.slice(-100);
|
|
309
|
-
}
|
|
310
|
-
|
|
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);
|
|
311
280
|
this.emit(sid, true);
|
|
312
281
|
return this.getSnapshot(sid);
|
|
313
282
|
}
|
|
@@ -319,16 +288,9 @@ export class GoalEngine {
|
|
|
319
288
|
|
|
320
289
|
goal.state = GoalState.CANCELLED;
|
|
321
290
|
goal.completedAt = Date.now();
|
|
322
|
-
goal
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
message: `Cancelled: ${reason}`,
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (goal.logs.length > 100) {
|
|
329
|
-
goal.logs = goal.logs.slice(-100);
|
|
330
|
-
}
|
|
331
|
-
|
|
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);
|
|
332
294
|
this.emit(sid, true);
|
|
333
295
|
return this.getSnapshot(sid);
|
|
334
296
|
}
|
|
@@ -351,38 +313,44 @@ export class GoalEngine {
|
|
|
351
313
|
goal.state = GoalState.COMPLETED;
|
|
352
314
|
goal.completedAt = now;
|
|
353
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' });
|
|
354
320
|
goal.logs.push({
|
|
355
321
|
timestamp: now,
|
|
356
322
|
type: 'info',
|
|
357
323
|
message: `Goal completed successfully: ${summary || 'All objectives met.'}`,
|
|
358
324
|
});
|
|
359
|
-
|
|
360
|
-
if (goal.logs.length > 100) {
|
|
361
|
-
goal.logs = goal.logs.slice(-100);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
325
|
for (const m of goal.milestones) {
|
|
365
326
|
if (m.status === MilestoneStatus.IN_PROGRESS || m.status === MilestoneStatus.PENDING) {
|
|
366
327
|
m.status = MilestoneStatus.COMPLETED;
|
|
367
328
|
}
|
|
368
329
|
}
|
|
369
|
-
|
|
370
330
|
this.stallCounters.delete(sid);
|
|
371
331
|
this.toolFailureCounters.delete(sid);
|
|
372
332
|
this.emit(sid, true);
|
|
373
333
|
return this.getSnapshot(sid);
|
|
374
334
|
}
|
|
375
335
|
|
|
336
|
+
setMilestones(milestonesList, sessionId = 'default') {
|
|
337
|
+
this.addMilestones(milestonesList, true, sessionId);
|
|
338
|
+
return this.getSnapshot(sessionId);
|
|
339
|
+
}
|
|
340
|
+
|
|
376
341
|
addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
|
|
377
342
|
const sid = sessionId || 'default';
|
|
378
343
|
const goal = this.goals.get(sid);
|
|
379
|
-
if (!goal || !Array.isArray(milestonesList)) return;
|
|
380
|
-
|
|
381
|
-
const newItems = parseMilestoneItems(milestonesList, goal.milestones.length);
|
|
382
|
-
goal.milestones.push(...newItems);
|
|
344
|
+
if (!goal || !Array.isArray(milestonesList)) return [];
|
|
383
345
|
|
|
346
|
+
const newMilestones = parseMilestoneItems(milestonesList, goal.milestones.length);
|
|
347
|
+
for (const item of newMilestones) {
|
|
348
|
+
goal.milestones.push(item);
|
|
349
|
+
}
|
|
384
350
|
this.recordProgress(sid);
|
|
351
|
+
recordActivity(goal, { type: 'milestone', tool: 'addMilestones', status: 'success', details: `Added ${newMilestones.length} milestones` });
|
|
385
352
|
if (shouldEmit) this.emit(sid);
|
|
353
|
+
return newMilestones;
|
|
386
354
|
}
|
|
387
355
|
|
|
388
356
|
updateMilestone(id, status, notes = '', sessionId = 'default', checklist = null) {
|
|
@@ -393,37 +361,33 @@ export class GoalEngine {
|
|
|
393
361
|
const target = goal.milestones.find((m) => matchMilestone(m, id));
|
|
394
362
|
if (!target) return false;
|
|
395
363
|
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const prevStatus = res.prevStatus;
|
|
379
|
+
recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'success', details: `${id} -> ${status}` });
|
|
403
380
|
|
|
404
|
-
// Auto Git Checkpoint on milestone completion
|
|
405
381
|
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
|
|
406
382
|
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
407
383
|
if (commitHash) {
|
|
408
384
|
target.checkpointCommit = commitHash;
|
|
409
|
-
goal.logs.push({
|
|
410
|
-
timestamp: Date.now(),
|
|
411
|
-
type: 'info',
|
|
412
|
-
message: `Git checkpoint saved for [${target.title}]: ${commitHash}`,
|
|
413
|
-
});
|
|
385
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Git checkpoint saved for [${target.title}]: ${commitHash}` });
|
|
414
386
|
}
|
|
415
387
|
}
|
|
416
388
|
|
|
417
|
-
goal.logs.push({
|
|
418
|
-
|
|
419
|
-
type: 'milestone',
|
|
420
|
-
message: `Milestone [${target.title}] status -> ${target.status}`,
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
if (goal.logs.length > 100) {
|
|
424
|
-
goal.logs = goal.logs.slice(-100);
|
|
425
|
-
}
|
|
426
|
-
|
|
389
|
+
goal.logs.push({ timestamp: Date.now(), type: 'milestone', message: `Milestone [${target.title}] status -> ${target.status}` });
|
|
390
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
427
391
|
this.recordProgress(sid);
|
|
428
392
|
this.emit(sid);
|
|
429
393
|
return true;
|
|
@@ -433,40 +397,22 @@ export class GoalEngine {
|
|
|
433
397
|
const sid = sessionId || 'default';
|
|
434
398
|
const goal = this.goals.get(sid);
|
|
435
399
|
if (!goal) return false;
|
|
436
|
-
|
|
437
400
|
const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
|
|
401
|
+
if (!target) return false;
|
|
438
402
|
const ok = toggleMilestoneChecklistItem(target, itemIndex, done);
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return
|
|
403
|
+
if (ok) {
|
|
404
|
+
this.recordProgress(sid);
|
|
405
|
+
this.emit(sid);
|
|
406
|
+
}
|
|
407
|
+
return ok;
|
|
444
408
|
}
|
|
445
409
|
|
|
446
410
|
extendBudget(addTokens = 50000, sessionId = 'default') {
|
|
447
411
|
const sid = sessionId || 'default';
|
|
448
412
|
const goal = this.goals.get(sid);
|
|
449
413
|
if (!goal) return false;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const currentMax = goal.maxTokenBudget || this.maxTokenBudget || 0;
|
|
453
|
-
const newBudget = Math.max(currentTotal, currentMax) + Math.max(1000, Number(addTokens) || 50000);
|
|
454
|
-
|
|
455
|
-
goal.maxTokenBudget = newBudget;
|
|
456
|
-
goal.budgetWarningTriggered = false;
|
|
457
|
-
|
|
458
|
-
if (goal.state === GoalState.PAUSED && goal.logs?.some((l) => l.message?.includes('Token budget reached'))) {
|
|
459
|
-
goal.state = GoalState.RUNNING;
|
|
460
|
-
goal.pausedAt = null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
goal.logs.push({
|
|
464
|
-
timestamp: Date.now(),
|
|
465
|
-
type: 'info',
|
|
466
|
-
message: `Token budget extended to ${newBudget.toLocaleString('en-US')} tokens`,
|
|
467
|
-
});
|
|
468
|
-
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
469
|
-
|
|
414
|
+
extendGoalBudget(goal, addTokens, this.maxTokenBudget);
|
|
415
|
+
recordActivity(goal, { type: 'budget', tool: 'extendBudget', status: 'info', details: `+${addTokens} tokens` });
|
|
470
416
|
this.emit(sid, true);
|
|
471
417
|
return true;
|
|
472
418
|
}
|
|
@@ -474,22 +420,20 @@ export class GoalEngine {
|
|
|
474
420
|
incrementIteration(sessionId = 'default') {
|
|
475
421
|
const sid = sessionId || 'default';
|
|
476
422
|
const goal = this.goals.get(sid);
|
|
477
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
423
|
+
if (!goal || goal.state !== GoalState.RUNNING) return false;
|
|
480
424
|
|
|
481
425
|
goal.iterationsCount += 1;
|
|
482
|
-
|
|
483
426
|
if (goal.iterationsCount >= goal.maxIterations) {
|
|
484
427
|
goal.state = GoalState.FAILED;
|
|
428
|
+
goal.failedAt = Date.now();
|
|
429
|
+
goal.failureReason = "Max iterations (" + goal.maxIterations + ") reached";
|
|
430
|
+
recordActivity(goal, { type: 'control', tool: 'incrementIteration', status: 'error', details: goal.failureReason });
|
|
485
431
|
goal.logs.push({
|
|
486
432
|
timestamp: Date.now(),
|
|
487
|
-
type: '
|
|
488
|
-
message: `
|
|
433
|
+
type: 'warning',
|
|
434
|
+
message: `Max iterations (${goal.maxIterations}) reached. Pausing autonomous loop.`,
|
|
489
435
|
});
|
|
490
|
-
if (goal.logs.length > 100)
|
|
491
|
-
goal.logs = goal.logs.slice(-100);
|
|
492
|
-
}
|
|
436
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
493
437
|
this.emit(sid, true);
|
|
494
438
|
return false;
|
|
495
439
|
}
|
|
@@ -508,10 +452,7 @@ export class GoalEngine {
|
|
|
508
452
|
const goal = this.goals.get(sid);
|
|
509
453
|
if (!goal) return;
|
|
510
454
|
|
|
511
|
-
if (!goal.tokensUsage) {
|
|
512
|
-
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
513
|
-
}
|
|
514
|
-
|
|
455
|
+
if (!goal.tokensUsage) goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
515
456
|
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
516
457
|
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
517
458
|
const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
|
|
@@ -520,43 +461,22 @@ export class GoalEngine {
|
|
|
520
461
|
goal.tokensUsage.completionTokens += completion;
|
|
521
462
|
goal.tokensUsage.totalTokens += total;
|
|
522
463
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
timestamp: Date.now(),
|
|
533
|
-
type: 'warning',
|
|
534
|
-
message: `Token budget warning: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens consumed (${Math.round((currentTotal / budget) * 100)}%)`,
|
|
535
|
-
});
|
|
536
|
-
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (currentTotal >= budget && goal.state === GoalState.RUNNING) {
|
|
540
|
-
goal.state = GoalState.PAUSED;
|
|
541
|
-
goal.pausedAt = Date.now();
|
|
542
|
-
goal.logs.push({
|
|
543
|
-
timestamp: Date.now(),
|
|
544
|
-
type: 'warning',
|
|
545
|
-
message: `Token budget reached: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens. Pausing autonomous loop.`,
|
|
546
|
-
});
|
|
547
|
-
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
548
|
-
this.emit(sid, true);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
464
|
+
const budgetCheck = processTokenBudget(goal, {
|
|
465
|
+
maxTokenBudget: this.maxTokenBudget,
|
|
466
|
+
budgetWarningThreshold: this.budgetWarningThreshold,
|
|
467
|
+
autoScaleBudgetNearCompletion: this.autoScaleBudgetNearCompletion,
|
|
468
|
+
GoalState,
|
|
469
|
+
});
|
|
470
|
+
if (budgetCheck.paused) {
|
|
471
|
+
this.emit(sid, true);
|
|
472
|
+
return;
|
|
551
473
|
}
|
|
552
|
-
|
|
553
474
|
this.emit(sid);
|
|
554
475
|
}
|
|
555
476
|
|
|
556
477
|
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
557
478
|
const sid = sessionId || 'default';
|
|
558
|
-
|
|
559
|
-
return calculateRemainingSeconds(goal, this.getElapsedSeconds(sid));
|
|
479
|
+
return calculateRemainingSeconds(this.goals.get(sid), this.getElapsedSeconds(sid));
|
|
560
480
|
}
|
|
561
481
|
|
|
562
482
|
getSnapshot(sessionId = 'default') {
|
|
@@ -571,17 +491,63 @@ export class GoalEngine {
|
|
|
571
491
|
getStatePromptInjection(sessionId = 'default') {
|
|
572
492
|
const sid = sessionId || 'default';
|
|
573
493
|
const goal = this.goals.get(sid);
|
|
574
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
575
|
-
|
|
494
|
+
if (!goal || goal.state !== GoalState.RUNNING) return '';
|
|
495
|
+
|
|
496
|
+
const snap = this.getSnapshot(sid);
|
|
497
|
+
const nudge = this.consumePendingNudge(sid);
|
|
498
|
+
const injection = buildStatePromptInjection(snap, nudge);
|
|
499
|
+
if (goal.userDirective) goal.userDirectiveConsumed = true;
|
|
500
|
+
return injection;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
intervene(intervention = {}, sessionId = 'default') {
|
|
504
|
+
const sid = sessionId || 'default';
|
|
505
|
+
const goal = this.goals.get(sid);
|
|
506
|
+
if (!goal) return { ok: false, error: 'Goal not found' };
|
|
507
|
+
const res = applyIntervention(goal, intervention);
|
|
508
|
+
this.emit(sid, true);
|
|
509
|
+
return { ok: true, state: this.getSnapshot(sid) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
approvePreplan(sessionId = 'default') {
|
|
513
|
+
const sid = sessionId || 'default';
|
|
514
|
+
const goal = this.goals.get(sid);
|
|
515
|
+
if (!goal || !goal.preplan?.active) return this.getSnapshot(sid);
|
|
516
|
+
const drafts = goal.preplan.draftMilestones || [];
|
|
517
|
+
goal.preplan.active = false;
|
|
518
|
+
if (drafts.length > 0 && goal.milestones.length === 0) {
|
|
519
|
+
this.addMilestones(drafts, false, sid);
|
|
576
520
|
}
|
|
521
|
+
this.emit(sid, true);
|
|
522
|
+
return this.getSnapshot(sid);
|
|
523
|
+
}
|
|
577
524
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
525
|
+
mergeGoalBranch(targetBranch = 'main', sessionId = 'default') {
|
|
526
|
+
const sid = sessionId || 'default';
|
|
527
|
+
const goal = this.goals.get(sid);
|
|
528
|
+
if (!goal || !goal.gitBranch) return false;
|
|
529
|
+
const ok = mergeGoalBranch(goal.cwd || process.cwd(), goal.gitBranch, targetBranch);
|
|
530
|
+
if (ok) recordActivity(goal, { type: 'git', tool: 'mergeGoalBranch', status: 'success', details: `Merged ${goal.gitBranch}` });
|
|
531
|
+
this.emit(sid, true);
|
|
532
|
+
return ok;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
discardGoalBranch(targetBranch = 'main', sessionId = 'default') {
|
|
536
|
+
const sid = sessionId || 'default';
|
|
537
|
+
const goal = this.goals.get(sid);
|
|
538
|
+
if (!goal || !goal.gitBranch) return false;
|
|
539
|
+
const ok = discardGoalBranch(goal.cwd || process.cwd(), goal.gitBranch, targetBranch);
|
|
540
|
+
if (ok) {
|
|
541
|
+
recordActivity(goal, { type: 'git', tool: 'discardGoalBranch', status: 'info', details: `Discarded ${goal.gitBranch}` });
|
|
542
|
+
goal.gitBranch = null;
|
|
583
543
|
}
|
|
544
|
+
this.emit(sid, true);
|
|
545
|
+
return ok;
|
|
546
|
+
}
|
|
584
547
|
|
|
585
|
-
|
|
548
|
+
recordToolActivity(tool, status, details, sessionId = 'default') {
|
|
549
|
+
const sid = sessionId || 'default';
|
|
550
|
+
const goal = this.goals.get(sid);
|
|
551
|
+
if (goal) recordActivity(goal, { type: 'tool', tool, status, details });
|
|
586
552
|
}
|
|
587
553
|
}
|