@goodandready/dsh-goal 0.2.9 → 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 +198 -233
- package/lib/index.js +36 -0
- package/lib/routes.js +52 -0
- 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,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,33 @@ 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
|
-
|
|
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}` });
|
|
404
380
|
|
|
405
|
-
// Auto Git Checkpoint on milestone completion
|
|
406
381
|
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
|
|
407
382
|
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
408
383
|
if (commitHash) {
|
|
409
384
|
target.checkpointCommit = commitHash;
|
|
410
|
-
goal.logs.push({
|
|
411
|
-
timestamp: Date.now(),
|
|
412
|
-
type: 'info',
|
|
413
|
-
message: `Git checkpoint saved for [${target.title}]: ${commitHash}`,
|
|
414
|
-
});
|
|
385
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Git checkpoint saved for [${target.title}]: ${commitHash}` });
|
|
415
386
|
}
|
|
416
387
|
}
|
|
417
388
|
|
|
418
|
-
goal.logs.push({
|
|
419
|
-
|
|
420
|
-
type: 'milestone',
|
|
421
|
-
message: `Milestone [${target.title}] status -> ${target.status}`,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
if (goal.logs.length > 100) {
|
|
425
|
-
goal.logs = goal.logs.slice(-100);
|
|
426
|
-
}
|
|
427
|
-
|
|
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);
|
|
428
391
|
this.recordProgress(sid);
|
|
429
392
|
this.emit(sid);
|
|
430
393
|
return true;
|
|
@@ -434,40 +397,22 @@ export class GoalEngine {
|
|
|
434
397
|
const sid = sessionId || 'default';
|
|
435
398
|
const goal = this.goals.get(sid);
|
|
436
399
|
if (!goal) return false;
|
|
437
|
-
|
|
438
400
|
const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
|
|
401
|
+
if (!target) return false;
|
|
439
402
|
const ok = toggleMilestoneChecklistItem(target, itemIndex, done);
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return
|
|
403
|
+
if (ok) {
|
|
404
|
+
this.recordProgress(sid);
|
|
405
|
+
this.emit(sid);
|
|
406
|
+
}
|
|
407
|
+
return ok;
|
|
445
408
|
}
|
|
446
409
|
|
|
447
410
|
extendBudget(addTokens = 50000, sessionId = 'default') {
|
|
448
411
|
const sid = sessionId || 'default';
|
|
449
412
|
const goal = this.goals.get(sid);
|
|
450
413
|
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
|
-
|
|
414
|
+
extendGoalBudget(goal, addTokens, this.maxTokenBudget);
|
|
415
|
+
recordActivity(goal, { type: 'budget', tool: 'extendBudget', status: 'info', details: `+${addTokens} tokens` });
|
|
471
416
|
this.emit(sid, true);
|
|
472
417
|
return true;
|
|
473
418
|
}
|
|
@@ -475,22 +420,20 @@ export class GoalEngine {
|
|
|
475
420
|
incrementIteration(sessionId = 'default') {
|
|
476
421
|
const sid = sessionId || 'default';
|
|
477
422
|
const goal = this.goals.get(sid);
|
|
478
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
423
|
+
if (!goal || goal.state !== GoalState.RUNNING) return false;
|
|
481
424
|
|
|
482
425
|
goal.iterationsCount += 1;
|
|
483
|
-
|
|
484
426
|
if (goal.iterationsCount >= goal.maxIterations) {
|
|
485
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 });
|
|
486
431
|
goal.logs.push({
|
|
487
432
|
timestamp: Date.now(),
|
|
488
|
-
type: '
|
|
489
|
-
message: `
|
|
433
|
+
type: 'warning',
|
|
434
|
+
message: `Max iterations (${goal.maxIterations}) reached. Pausing autonomous loop.`,
|
|
490
435
|
});
|
|
491
|
-
if (goal.logs.length > 100)
|
|
492
|
-
goal.logs = goal.logs.slice(-100);
|
|
493
|
-
}
|
|
436
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
494
437
|
this.emit(sid, true);
|
|
495
438
|
return false;
|
|
496
439
|
}
|
|
@@ -509,10 +452,7 @@ export class GoalEngine {
|
|
|
509
452
|
const goal = this.goals.get(sid);
|
|
510
453
|
if (!goal) return;
|
|
511
454
|
|
|
512
|
-
if (!goal.tokensUsage) {
|
|
513
|
-
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
514
|
-
}
|
|
515
|
-
|
|
455
|
+
if (!goal.tokensUsage) goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
516
456
|
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
517
457
|
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
518
458
|
const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
|
|
@@ -521,43 +461,22 @@ export class GoalEngine {
|
|
|
521
461
|
goal.tokensUsage.completionTokens += completion;
|
|
522
462
|
goal.tokensUsage.totalTokens += total;
|
|
523
463
|
|
|
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
|
-
}
|
|
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;
|
|
552
473
|
}
|
|
553
|
-
|
|
554
474
|
this.emit(sid);
|
|
555
475
|
}
|
|
556
476
|
|
|
557
477
|
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
558
478
|
const sid = sessionId || 'default';
|
|
559
|
-
|
|
560
|
-
return calculateRemainingSeconds(goal, this.getElapsedSeconds(sid));
|
|
479
|
+
return calculateRemainingSeconds(this.goals.get(sid), this.getElapsedSeconds(sid));
|
|
561
480
|
}
|
|
562
481
|
|
|
563
482
|
getSnapshot(sessionId = 'default') {
|
|
@@ -572,17 +491,63 @@ export class GoalEngine {
|
|
|
572
491
|
getStatePromptInjection(sessionId = 'default') {
|
|
573
492
|
const sid = sessionId || 'default';
|
|
574
493
|
const goal = this.goals.get(sid);
|
|
575
|
-
if (!goal || goal.state !== GoalState.RUNNING)
|
|
576
|
-
|
|
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);
|
|
577
520
|
}
|
|
521
|
+
this.emit(sid, true);
|
|
522
|
+
return this.getSnapshot(sid);
|
|
523
|
+
}
|
|
578
524
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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;
|
|
584
543
|
}
|
|
544
|
+
this.emit(sid, true);
|
|
545
|
+
return ok;
|
|
546
|
+
}
|
|
585
547
|
|
|
586
|
-
|
|
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 });
|
|
587
552
|
}
|
|
588
553
|
}
|