@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.
@@ -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,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
- timestamp: now,
245
- type: 'info',
246
- message: `Goal initiated: "${cleanTitle}"` + (gitCommit ? ` (Git: ${gitCommit})` : ''),
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.logs.push({
272
- timestamp: Date.now(),
273
- type: 'warning',
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
- timestamp: now,
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.logs.push({
323
- timestamp: Date.now(),
324
- type: 'warning',
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 { prevStatus } = applyMilestoneUpdate(
397
- target,
398
- status,
399
- notes,
400
- checklist,
401
- Object.values(MilestoneStatus)
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
- timestamp: Date.now(),
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 (!ok) return false;
440
-
441
- this.recordProgress(sid);
442
- this.emit(sid);
443
- return true;
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
- const currentTotal = goal.tokensUsage?.totalTokens || 0;
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: 'error',
488
- message: `Safety limit reached: maximum ${goal.maxIterations} iterations exceeded.`,
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
- // Token Budget Guard Check
524
- const budget = goal.maxTokenBudget || this.maxTokenBudget || 0;
525
- if (budget > 0) {
526
- const currentTotal = goal.tokensUsage.totalTokens;
527
- const warnThreshold = (budget * (this.budgetWarningThreshold || 80)) / 100;
528
-
529
- if (currentTotal >= warnThreshold && !goal.budgetWarningTriggered) {
530
- goal.budgetWarningTriggered = true;
531
- goal.logs.push({
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
- const goal = this.goals.get(sid);
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
- 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);
576
520
  }
521
+ this.emit(sid, true);
522
+ return this.getSnapshot(sid);
523
+ }
577
524
 
578
- const snapshot = this.getSnapshot(sid);
579
- let pendingNudge = null;
580
- if (goal.pendingNudge) {
581
- pendingNudge = goal.pendingNudge;
582
- 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;
583
543
  }
544
+ this.emit(sid, true);
545
+ return ok;
546
+ }
584
547
 
585
- 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 });
586
552
  }
587
553
  }