@goodandready/dsh-goal 0.2.5 → 0.2.7

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.
@@ -1,10 +1,11 @@
1
- import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
2
- import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR } from './engine-reports.js';
1
+ import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot } from './goal-engine-constants.js';
2
+ import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
3
3
  import { EngineStore } from './engine-store.js';
4
4
  import { buildStatePromptInjection } from './engine-prompt.js';
5
+ import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
5
6
 
6
- export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf };
7
- export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR };
7
+ export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone };
8
+ export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
8
9
 
9
10
  /**
10
11
  * Isolated Goal Management Engine for DeepSeek Harness
@@ -16,6 +17,9 @@ export class GoalEngine {
16
17
  this.enableSound = options.enableSound ?? true;
17
18
  this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
18
19
  this.consecutiveToolFailureLimit = options.consecutiveToolFailureLimit ?? 3;
20
+ this.maxTokenBudget = options.maxTokenBudget ?? 0;
21
+ this.budgetWarningThreshold = options.budgetWarningThreshold ?? 80;
22
+ this.autoCheckpointOnMilestone = options.autoCheckpointOnMilestone ?? false;
19
23
  this.maxSessions = options.maxSessions ?? 100;
20
24
  this.goals = new Map();
21
25
  this.listeners = new Set();
@@ -99,6 +103,15 @@ export class GoalEngine {
99
103
  if (typeof config.consecutiveToolFailureLimit === 'number') {
100
104
  this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
101
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
+ }
102
115
  this.emit();
103
116
  }
104
117
 
@@ -175,6 +188,7 @@ export class GoalEngine {
175
188
  const oldest = inactive.shift();
176
189
  this.goals.delete(oldest.sid);
177
190
  this.stallCounters.delete(oldest.sid);
191
+ this.toolFailureCounters.delete(oldest.sid);
178
192
  }
179
193
  }
180
194
 
@@ -218,6 +232,8 @@ export class GoalEngine {
218
232
  completedAt: null,
219
233
  iterationsCount: 0,
220
234
  maxIterations: options.maxIterations ?? this.defaultMaxIterations,
235
+ maxTokenBudget: options.maxTokenBudget ?? this.maxTokenBudget,
236
+ budgetWarningTriggered: false,
221
237
  gitStartCommit: gitCommit || null,
222
238
  pendingNudge: null,
223
239
  nudges: [],
@@ -321,6 +337,7 @@ export class GoalEngine {
321
337
  const sid = sessionId || 'default';
322
338
  this.goals.delete(sid);
323
339
  this.stallCounters.delete(sid);
340
+ this.toolFailureCounters.delete(sid);
324
341
  this.emit(sid, true);
325
342
  return this.getSnapshot(sid);
326
343
  }
@@ -351,6 +368,7 @@ export class GoalEngine {
351
368
  }
352
369
 
353
370
  this.stallCounters.delete(sid);
371
+ this.toolFailureCounters.delete(sid);
354
372
  this.emit(sid, true);
355
373
  return this.getSnapshot(sid);
356
374
  }
@@ -360,36 +378,40 @@ export class GoalEngine {
360
378
  const goal = this.goals.get(sid);
361
379
  if (!goal || !Array.isArray(milestonesList)) return;
362
380
 
363
- for (const item of milestonesList) {
364
- const itemTitle = typeof item === 'string' ? item : item.title;
365
- if (!itemTitle || !itemTitle.trim()) continue;
366
-
367
- const mId = typeof item === 'object' && item.id ? item.id : `m-${goal.milestones.length + 1}`;
368
- goal.milestones.push({
369
- id: String(mId),
370
- title: itemTitle.trim(),
371
- status: typeof item === 'object' && item.status ? item.status : MilestoneStatus.PENDING,
372
- notes: typeof item === 'object' && item.notes ? item.notes : '',
373
- });
374
- }
381
+ const newItems = parseMilestoneItems(milestonesList, goal.milestones.length);
382
+ goal.milestones.push(...newItems);
375
383
 
376
384
  this.recordProgress(sid);
377
385
  if (shouldEmit) this.emit(sid);
378
386
  }
379
387
 
380
- updateMilestone(id, status, notes = '', sessionId = 'default') {
388
+ updateMilestone(id, status, notes = '', sessionId = 'default', checklist = null) {
381
389
  const sid = sessionId || 'default';
382
390
  const goal = this.goals.get(sid);
383
391
  if (!goal) return false;
384
392
 
385
- const target = goal.milestones.find((m) => m.id === String(id));
393
+ const target = goal.milestones.find((m) => matchMilestone(m, id));
386
394
  if (!target) return false;
387
395
 
388
- if (status && Object.values(MilestoneStatus).includes(status)) {
389
- target.status = status;
390
- }
391
- if (notes) {
392
- target.notes = String(notes);
396
+ const { prevStatus } = applyMilestoneUpdate(
397
+ target,
398
+ status,
399
+ notes,
400
+ checklist,
401
+ Object.values(MilestoneStatus)
402
+ );
403
+
404
+ // Auto Git Checkpoint on milestone completion
405
+ if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
406
+ const commitHash = createMilestoneCheckpoint(target, sid);
407
+ if (commitHash) {
408
+ target.checkpointCommit = commitHash;
409
+ goal.logs.push({
410
+ timestamp: Date.now(),
411
+ type: 'info',
412
+ message: `Git checkpoint saved for [${target.title}]: ${commitHash}`,
413
+ });
414
+ }
393
415
  }
394
416
 
395
417
  goal.logs.push({
@@ -407,6 +429,48 @@ export class GoalEngine {
407
429
  return true;
408
430
  }
409
431
 
432
+ toggleChecklistItem(milestoneId, itemIndex, done, sessionId = 'default') {
433
+ const sid = sessionId || 'default';
434
+ const goal = this.goals.get(sid);
435
+ if (!goal) return false;
436
+
437
+ const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
438
+ const ok = toggleMilestoneChecklistItem(target, itemIndex, done);
439
+ if (!ok) return false;
440
+
441
+ this.recordProgress(sid);
442
+ this.emit(sid);
443
+ return true;
444
+ }
445
+
446
+ extendBudget(addTokens = 50000, sessionId = 'default') {
447
+ const sid = sessionId || 'default';
448
+ const goal = this.goals.get(sid);
449
+ 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
+
470
+ this.emit(sid, true);
471
+ return true;
472
+ }
473
+
410
474
  incrementIteration(sessionId = 'default') {
411
475
  const sid = sessionId || 'default';
412
476
  const goal = this.goals.get(sid);
@@ -435,13 +499,7 @@ export class GoalEngine {
435
499
  }
436
500
 
437
501
  getElapsedSeconds(sessionId = 'default') {
438
- const sid = sessionId || 'default';
439
- const goal = this.goals.get(sid);
440
- if (!goal) return 0;
441
- const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = goal;
442
- const endTime = completedAt || (pausedAt || Date.now());
443
- const elapsedMs = Math.max(0, endTime - startedAt - totalPausedDurationMs);
444
- return Math.floor(elapsedMs / 1000);
502
+ return calculateElapsedSeconds(this.goals.get(sessionId || 'default'));
445
503
  }
446
504
 
447
505
  addTokenUsage(usage, sessionId = 'default') {
@@ -462,98 +520,48 @@ export class GoalEngine {
462
520
  goal.tokensUsage.completionTokens += completion;
463
521
  goal.tokensUsage.totalTokens += total;
464
522
 
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
+ }
551
+ }
552
+
465
553
  this.emit(sid);
466
554
  }
467
555
 
468
556
  getEstimatedRemainingSeconds(sessionId = 'default') {
469
557
  const sid = sessionId || 'default';
470
558
  const goal = this.goals.get(sid);
471
- if (!goal || goal.state !== GoalState.RUNNING) return null;
472
-
473
- const total = goal.milestones.length;
474
- if (total === 0) return null;
475
-
476
- const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
477
- if (completedCount === 0 || completedCount >= total) return null;
478
-
479
- const elapsed = this.getElapsedSeconds(sid);
480
- if (elapsed <= 0) return null;
481
-
482
- const avgSecPerMilestone = elapsed / completedCount;
483
- const remainingCount = total - completedCount;
484
- return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
559
+ return calculateRemainingSeconds(goal, this.getElapsedSeconds(sid));
485
560
  }
486
561
 
487
562
  getSnapshot(sessionId = 'default') {
488
563
  const sid = sessionId || 'default';
489
- const goal = this.goals.get(sid);
490
- if (!goal) {
491
- return {
492
- sessionId: sid,
493
- hasActiveGoal: false,
494
- state: GoalState.IDLE,
495
- title: '',
496
- startedAt: null,
497
- pausedAt: null,
498
- totalPausedDurationMs: 0,
499
- completedAt: null,
500
- elapsedSeconds: 0,
501
- formattedElapsed: '0s',
502
- estimatedRemainingSeconds: null,
503
- formattedETA: null,
504
- lang: 'en',
505
- tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
506
- milestones: [],
507
- progressPercent: 0,
508
- iterationsCount: 0,
509
- maxIterations: this.defaultMaxIterations,
510
- autoDrive: this.autoDrive,
511
- enableSound: this.enableSound,
512
- showQuickLaunchButton: this.showQuickLaunchButton,
513
- gitStartCommit: null,
514
- pendingNudge: null,
515
- toolFailureCount: 0,
516
- consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
517
- };
518
- }
519
-
520
- const elapsed = this.getElapsedSeconds(sid);
521
- const milestones = goal.milestones;
522
- const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
523
- const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
524
- const estSec = this.getEstimatedRemainingSeconds(sid);
525
-
526
- return {
527
- sessionId: sid,
528
- hasActiveGoal: true,
529
- id: goal.id,
530
- state: goal.state,
531
- title: goal.title,
532
- description: goal.description,
533
- lang: goal.lang || detectLanguage(goal.title),
534
- startedAt: goal.startedAt,
535
- pausedAt: goal.pausedAt,
536
- totalPausedDurationMs: goal.totalPausedDurationMs,
537
- completedAt: goal.completedAt,
538
- elapsedSeconds: elapsed,
539
- formattedElapsed: formatElapsed(elapsed),
540
- estimatedRemainingSeconds: estSec,
541
- formattedETA: formatETA(estSec),
542
- tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
543
- iterationsCount: goal.iterationsCount,
544
- maxIterations: goal.maxIterations,
545
- milestones,
546
- progressPercent,
547
- logs: goal.logs,
548
- resultSummary: goal.resultSummary,
549
- autoDrive: this.autoDrive,
550
- enableSound: this.enableSound,
551
- showQuickLaunchButton: this.showQuickLaunchButton,
552
- gitStartCommit: goal.gitStartCommit || null,
553
- pendingNudge: goal.pendingNudge || null,
554
- toolFailureCount: this.getToolFailureCount(sid),
555
- consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
556
- };
564
+ return buildSnapshot(this.goals.get(sid), sid, this);
557
565
  }
558
566
 
559
567
  getGoalSnapshot(sessionId = 'default') {
package/lib/index.js CHANGED
@@ -19,6 +19,9 @@ export const Config = z.object({
19
19
  showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
20
20
  consecutiveToolFailureLimit: z.number().default(3).description('Auto-pause goal if N consecutive turns encounter tool execution errors (0 to disable)'),
21
21
  enableBrowserNotifications: z.boolean().default(true).description('Show desktop notifications on goal completion or failure'),
22
+ maxTokenBudget: z.number().default(0).description('Maximum token budget per goal session (0 to disable)'),
23
+ budgetWarningThreshold: z.number().default(80).description('Percentage of token budget consumed before model warning injection (e.g. 80)'),
24
+ autoCheckpointOnMilestone: z.boolean().default(false).description('Automatically create git commit checkpoint upon milestone completion'),
22
25
  storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
23
26
  });
24
27
 
@@ -37,6 +40,9 @@ export function apply(ctx, config = {}) {
37
40
  showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
38
41
  consecutiveToolFailureLimit: config?.consecutiveToolFailureLimit ?? 3,
39
42
  enableBrowserNotifications: config?.enableBrowserNotifications ?? true,
43
+ maxTokenBudget: config?.maxTokenBudget ?? 0,
44
+ budgetWarningThreshold: config?.budgetWarningThreshold ?? 80,
45
+ autoCheckpointOnMilestone: config?.autoCheckpointOnMilestone ?? false,
40
46
  storagePath: config?.storagePath,
41
47
  };
42
48
 
@@ -51,6 +57,9 @@ export function apply(ctx, config = {}) {
51
57
  enableSound: currentSettings.enableSound,
52
58
  showQuickLaunchButton: currentSettings.showQuickLaunchButton,
53
59
  consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
60
+ maxTokenBudget: currentSettings.maxTokenBudget,
61
+ budgetWarningThreshold: currentSettings.budgetWarningThreshold,
62
+ autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
54
63
  storagePath,
55
64
  });
56
65
 
@@ -177,6 +186,9 @@ export function apply(ctx, config = {}) {
177
186
  showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
178
187
  consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
179
188
  enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
189
+ maxTokenBudget: typeof live.maxTokenBudget === 'number' ? live.maxTokenBudget : currentSettings.maxTokenBudget,
190
+ budgetWarningThreshold: typeof live.budgetWarningThreshold === 'number' ? live.budgetWarningThreshold : currentSettings.budgetWarningThreshold,
191
+ autoCheckpointOnMilestone: typeof live.autoCheckpointOnMilestone === 'boolean' ? live.autoCheckpointOnMilestone : currentSettings.autoCheckpointOnMilestone,
180
192
  storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
181
193
  };
182
194
  }
@@ -205,6 +217,9 @@ export function apply(ctx, config = {}) {
205
217
  enableSound: live.enableSound,
206
218
  showQuickLaunchButton: live.showQuickLaunchButton,
207
219
  consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
220
+ maxTokenBudget: live.maxTokenBudget,
221
+ budgetWarningThreshold: live.budgetWarningThreshold,
222
+ autoCheckpointOnMilestone: live.autoCheckpointOnMilestone,
208
223
  });
209
224
  };
210
225
 
package/lib/routes.js CHANGED
@@ -1,4 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
4
+ import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
5
+
6
+ function getSafeCwd(requestedCwd) {
7
+ if (!requestedCwd || typeof requestedCwd !== 'string') {
8
+ return process.cwd();
9
+ }
10
+ try {
11
+ const resolved = path.resolve(requestedCwd.trim());
12
+ const st = fs.statSync(resolved);
13
+ if (!st.isDirectory()) {
14
+ return null;
15
+ }
16
+ return resolved;
17
+ } catch (err) {
18
+ return null;
19
+ }
20
+ }
2
21
 
3
22
  /**
4
23
  * Register webServer HTTP routes and SSE stream for DSH Goal
@@ -247,7 +266,7 @@ export function registerRoutes(ctx, {
247
266
  res.statusCode = 400;
248
267
  return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
249
268
  }
250
- const ok = engine.updateMilestone(milestoneId, status, notes, sid);
269
+ const ok = engine.updateMilestone(milestoneId, status, notes, sid, data.checklist);
251
270
  if (!ok) {
252
271
  res.statusCode = 404;
253
272
  return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
@@ -256,6 +275,74 @@ export function registerRoutes(ctx, {
256
275
  break;
257
276
  }
258
277
 
278
+ case 'toggle_checklist_item': {
279
+ if (!milestoneId || data.itemIndex === undefined) {
280
+ res.statusCode = 400;
281
+ return res.end(JSON.stringify({ error: 'milestoneId and itemIndex are required' }));
282
+ }
283
+ const ok = engine.toggleChecklistItem(milestoneId, data.itemIndex, data.done, sid);
284
+ if (!ok) {
285
+ res.statusCode = 404;
286
+ return res.end(JSON.stringify({ error: `Milestone checklist item not found` }));
287
+ }
288
+ result = engine.getSnapshot(sid);
289
+ break;
290
+ }
291
+
292
+ case 'extend_budget': {
293
+ const addTokens = data.addTokens || 50000;
294
+ const ok = engine.extendBudget(addTokens, sid);
295
+ if (!ok) {
296
+ res.statusCode = 404;
297
+ return res.end(JSON.stringify({ error: 'No active goal found to extend budget' }));
298
+ }
299
+ if (typeof resumeActiveAgent === 'function') {
300
+ resumeActiveAgent(undefined, sid);
301
+ }
302
+ result = engine.getSnapshot(sid);
303
+ break;
304
+ }
305
+
306
+ case 'rollback_milestone': {
307
+ const commit = data.commit || data.checkpointCommit;
308
+ if (!commit) {
309
+ res.statusCode = 400;
310
+ return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
311
+ }
312
+ const safeCwd = getSafeCwd(data.cwd);
313
+ if (data.cwd && !safeCwd) {
314
+ res.statusCode = 400;
315
+ return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
316
+ }
317
+ const ok = rollbackToCheckpoint(commit, safeCwd);
318
+ if (!ok) {
319
+ res.statusCode = 500;
320
+ return res.end(JSON.stringify({ error: `Failed to rollback to checkpoint ${commit}` }));
321
+ }
322
+ result = engine.getSnapshot(sid);
323
+ break;
324
+ }
325
+
326
+ case 'save_artifact': {
327
+ const snap = engine.getSnapshot(sid);
328
+ if (!snap || !snap.hasActiveGoal) {
329
+ res.statusCode = 404;
330
+ return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
331
+ }
332
+ const safeCwd = getSafeCwd(data.cwd);
333
+ if (data.cwd && !safeCwd) {
334
+ res.statusCode = 400;
335
+ return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
336
+ }
337
+ const artifact = saveGoalArtifact(snap, safeCwd);
338
+ if (!artifact) {
339
+ res.statusCode = 500;
340
+ return res.end(JSON.stringify({ error: 'Failed to write goal artifact to disk' }));
341
+ }
342
+ res.statusCode = 200;
343
+ return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
344
+ }
345
+
259
346
  default:
260
347
  res.statusCode = 400;
261
348
  return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
package/lib/tools.js CHANGED
@@ -229,13 +229,17 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
229
229
  });
230
230
 
231
231
  // Tool 5: goal_update_progress
232
- const handleUpdateProgress = async ({ milestone_id, status, notes }, toolCtx) => {
232
+ const handleUpdateProgress = async ({ milestone_id, status, notes, checklist }, toolCtx) => {
233
233
  const sid = sessionIdOf(toolCtx, 'default');
234
- const snap = engine.updateMilestone(milestone_id, status, notes, sid);
234
+ engine.updateMilestone(milestone_id, status, notes, sid, checklist);
235
+ const snap = engine.getSnapshot(sid);
236
+ const targetMilestone = snap.milestones?.find((m) => m.id === String(milestone_id) || m.id.toLowerCase().replace(/[^a-z0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^a-z0-9]/g, '') || m.id.toLowerCase().replace(/[^0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^0-9]/g, ''));
235
237
  return {
236
238
  success: true,
237
239
  milestone_id,
238
240
  status,
241
+ checklist: targetMilestone?.checklist || null,
242
+ checkpointCommit: targetMilestone?.checkpointCommit || null,
239
243
  total_milestones: snap.milestones?.length,
240
244
  completed: snap.milestones?.filter((m) => m.status === 'completed').length,
241
245
  };
@@ -243,7 +247,7 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
243
247
 
244
248
  safeRegister({
245
249
  name: 'goal_update_progress',
246
- description: 'Update the progress of a specific milestone.',
250
+ description: 'Update the progress of a specific milestone, optionally attaching a sub-tasks checklist.',
247
251
  parameters: {
248
252
  type: 'object',
249
253
  properties: {
@@ -254,6 +258,18 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
254
258
  description: 'New milestone status',
255
259
  },
256
260
  notes: { type: 'string', description: 'Summary of actions completed or blocking issue' },
261
+ checklist: {
262
+ type: 'array',
263
+ items: {
264
+ type: 'object',
265
+ properties: {
266
+ text: { type: 'string', description: 'Checklist task item description' },
267
+ done: { type: 'boolean', description: 'Whether the item is completed' },
268
+ },
269
+ required: ['text', 'done'],
270
+ },
271
+ description: 'Optional sub-tasks / checklist items within this milestone',
272
+ },
257
273
  },
258
274
  required: ['milestone_id', 'status'],
259
275
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Autonomous Goal Execution & Multi-Turn Task Tracking Engine with Sticky Header for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",