@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.
@@ -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?.('[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,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 { prevStatus } = applyMilestoneUpdate(
398
- target,
399
- status,
400
- notes,
401
- checklist,
402
- Object.values(MilestoneStatus)
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
- timestamp: Date.now(),
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 (!ok) return false;
441
-
442
- this.recordProgress(sid);
443
- this.emit(sid);
444
- return true;
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
- 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
-
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: 'error',
489
- message: `Safety limit reached: maximum ${goal.maxIterations} iterations exceeded.`,
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
- // 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
- }
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
- const goal = this.goals.get(sid);
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
- return '';
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
- const snapshot = this.getSnapshot(sid);
580
- let pendingNudge = null;
581
- if (goal.pendingNudge) {
582
- pendingNudge = goal.pendingNudge;
583
- goal.pendingNudge = null;
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
- return buildStatePromptInjection(snapshot, pendingNudge);
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
  }