@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.
@@ -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?.('[GoalEngine] Listener error:', err?.message || err);
76
+ console.error('[dsh-goal] Error in GoalEngine listener:', err);
62
77
  }
63
78
  }
64
79
  }
65
80
 
66
81
  recordProgress(sessionId = 'default') {
67
- const sid = sessionId || 'default';
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
- const sid = sessionId || 'default';
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
- this.autoDrive = config.autoDrive;
96
- }
97
- if (typeof config.enableSound === 'boolean') {
98
- this.enableSound = config.enableSound;
99
- }
100
- if (typeof config.showQuickLaunchButton === 'boolean') {
101
- this.showQuickLaunchButton = config.showQuickLaunchButton;
102
- }
103
- if (typeof config.consecutiveToolFailureLimit === 'number') {
104
- this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
105
- }
106
- if (typeof config.maxTokenBudget === 'number') {
107
- this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
108
- }
109
- if (typeof config.budgetWarningThreshold === 'number') {
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 current = this.toolFailureCounters.get(sid) || 0;
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
- const sid = sessionId || 'default';
132
- this.toolFailureCounters.set(sid, 0);
136
+ this.toolFailureCounters.set(sessionId || 'default', 0);
133
137
  }
134
138
 
135
139
  getToolFailureCount(sessionId = 'default') {
136
- const sid = sessionId || 'default';
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
- const clean = String(text || '').trim();
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 || !goal.pendingNudge) return null;
171
- const nudge = goal.pendingNudge;
172
- goal.pendingNudge = null;
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
- if (this.goals.size < this.maxSessions) return;
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
- timestamp: now,
246
- type: 'info',
247
- message: `Goal initiated: "${cleanTitle}"` + (gitCommit ? ` (Git: ${gitCommit})` : ''),
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.logs.push({
273
- timestamp: Date.now(),
274
- type: 'warning',
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
- timestamp: now,
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.logs.push({
324
- timestamp: Date.now(),
325
- type: 'warning',
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 { prevStatus } = applyMilestoneUpdate(
398
- target,
399
- status,
400
- notes,
401
- checklist,
402
- Object.values(MilestoneStatus)
403
- );
404
-
405
- // Auto Git Checkpoint on milestone completion
406
- if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
407
- const commitHash = createMilestoneCheckpoint(target, sid);
408
- if (commitHash) {
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
- goal.logs.push({
419
- timestamp: Date.now(),
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 (goal.logs.length > 100) {
425
- goal.logs = goal.logs.slice(-100);
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 (!ok) return false;
441
-
442
- this.recordProgress(sid);
443
- this.emit(sid);
444
- return true;
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
- const currentTotal = goal.tokensUsage?.totalTokens || 0;
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: 'error',
489
- message: `Safety limit reached: maximum ${goal.maxIterations} iterations exceeded.`,
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
- // Token Budget Guard Check
525
- const budget = goal.maxTokenBudget || this.maxTokenBudget || 0;
526
- if (budget > 0) {
527
- const currentTotal = goal.tokensUsage.totalTokens;
528
- const warnThreshold = (budget * (this.budgetWarningThreshold || 80)) / 100;
529
-
530
- if (currentTotal >= warnThreshold && !goal.budgetWarningTriggered) {
531
- goal.budgetWarningTriggered = true;
532
- goal.logs.push({
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
- const goal = this.goals.get(sid);
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
- return '';
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
- const snapshot = this.getSnapshot(sid);
580
- let pendingNudge = null;
581
- if (goal.pendingNudge) {
582
- pendingNudge = goal.pendingNudge;
583
- goal.pendingNudge = null;
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
- return buildStatePromptInjection(snapshot, pendingNudge);
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
  }