@goodandready/dsh-goal 0.2.29 → 0.2.31

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,4 +1,7 @@
1
1
  import { verifyMilestoneAcceptance } from './engine-verification.js';
2
+ import { getEngineGoalDiff } from './engine-diff.js';
3
+ import { syncGoalToKanban, syncMilestoneStatusToKanban, handleKanbanCardMoved } from './engine-kanban-sync.js';
4
+ import { askOperatorQuery, answerOperatorQuery } from './engine-operator-query.js';
2
5
  import { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot, getSafeCwd } from './goal-engine-constants.js';
3
6
  import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
4
7
  import { EngineStore } from './engine-store.js';
@@ -11,8 +14,7 @@ import { generateRetrospectiveData } from './engine-retrospective.js';
11
14
  import { generatePreplanDraft } from './engine-templates.js';
12
15
  import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget, setGoalMode, pauseGoal, resumeGoal, cancelGoal } from './engine-sessions.js';
13
16
 
14
- export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist };
15
- export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
17
+ export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
16
18
 
17
19
  /**
18
20
  * Isolated Goal Management Engine for DeepSeek Harness
@@ -22,25 +24,19 @@ export class GoalEngine {
22
24
  this.logger = options.logger || console;
23
25
  const rawDefIters = options.defaultMaxIterations ?? 25;
24
26
  this.defaultMaxIterations = typeof rawDefIters === "number" ? rawDefIters : (typeof rawDefIters?.value === "number" ? rawDefIters.value : 25);
25
- this.autoDrive = options.autoDrive ?? true;
26
- this.enableSound = options.enableSound ?? true;
27
- this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
28
- this.consecutiveToolFailureLimit = options.consecutiveToolFailureLimit ?? 3;
29
- this.maxTokenBudget = options.maxTokenBudget ?? 0;
30
- this.budgetWarningThreshold = options.budgetWarningThreshold ?? 80;
31
- this.autoCheckpointOnMilestone = options.autoCheckpointOnMilestone ?? false;
32
- this.enableLiveActivityFeed = options.enableLiveActivityFeed ?? true;
33
- this.enablePreplanning = options.enablePreplanning ?? true;
34
- this.autoBranchOnGoalStart = options.autoBranchOnGoalStart ?? true;
35
- this.enablePostGoalRetrospective = options.enablePostGoalRetrospective ?? true;
36
- this.autoScaleBudgetNearCompletion = options.autoScaleBudgetNearCompletion ?? false;
37
- this.enableTemplatesDrawer = options.enableTemplatesDrawer ?? true;
38
- this.enableMilestoneDependencies = options.enableMilestoneDependencies ?? true;
39
- this.soundScheme = options.soundScheme ?? 'default';
40
- this.enableVoiceAnnouncements = options.enableVoiceAnnouncements ?? false;
41
- this.approvalGate = options.approvalGate || null;
42
- this.checkApproval = options.checkApproval || null;
43
- this.maxSessions = options.maxSessions ?? 100;
27
+ Object.assign(this, {
28
+ autoDrive: options.autoDrive ?? true, enableSound: options.enableSound ?? true,
29
+ showQuickLaunchButton: options.showQuickLaunchButton ?? true,
30
+ consecutiveToolFailureLimit: options.consecutiveToolFailureLimit ?? 3,
31
+ maxTokenBudget: options.maxTokenBudget ?? 0, budgetWarningThreshold: options.budgetWarningThreshold ?? 80,
32
+ autoCheckpointOnMilestone: options.autoCheckpointOnMilestone ?? false,
33
+ enableLiveActivityFeed: options.enableLiveActivityFeed ?? true, enablePreplanning: options.enablePreplanning ?? true,
34
+ autoBranchOnGoalStart: options.autoBranchOnGoalStart ?? true, enablePostGoalRetrospective: options.enablePostGoalRetrospective ?? true,
35
+ autoScaleBudgetNearCompletion: options.autoScaleBudgetNearCompletion ?? false, enableTemplatesDrawer: options.enableTemplatesDrawer ?? true,
36
+ enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default', enableKanbanSync: options.enableKanbanSync ?? true, ctx: options.ctx || null, kanbanService: options.kanbanService || null, kanbanStore: options.kanbanStore || null,
37
+ enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, approvalGate: options.approvalGate || null,
38
+ checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100,
39
+ });
44
40
  this.goals = new Map();
45
41
  this.listeners = new Set();
46
42
  this.stallCounters = new Map();
@@ -54,10 +50,7 @@ export class GoalEngine {
54
50
  set storagePath(val) { this.store.setStoragePath(val); }
55
51
  get saveTimer() { return this.store.saveTimer; }
56
52
  get currentGoal() { return this.goals.get('default') || null; }
57
- set currentGoal(val) {
58
- if (val) this.goals.set('default', val);
59
- else this.goals.delete('default');
60
- }
53
+ set currentGoal(val) { if (val) this.goals.set('default', val); else this.goals.delete('default'); }
61
54
 
62
55
  loadStateFromDisk() { this.store.loadStateFromDisk(); }
63
56
  scheduleSave(immediate = false) { this.store.scheduleSave(immediate); }
@@ -111,22 +104,12 @@ export class GoalEngine {
111
104
  }
112
105
  }
113
106
  }
114
- if (typeof config.autoDrive === 'boolean') this.autoDrive = config.autoDrive;
115
- if (typeof config.enableSound === 'boolean') this.enableSound = config.enableSound;
116
- if (typeof config.showQuickLaunchButton === 'boolean') this.showQuickLaunchButton = config.showQuickLaunchButton;
107
+ const boolKeys = ['autoDrive', 'enableSound', 'showQuickLaunchButton', 'autoCheckpointOnMilestone', 'enableLiveActivityFeed', 'enablePreplanning', 'autoBranchOnGoalStart', 'enablePostGoalRetrospective', 'autoScaleBudgetNearCompletion', 'enableTemplatesDrawer', 'enableMilestoneDependencies', 'enableVoiceAnnouncements'];
108
+ for (const k of boolKeys) if (typeof config[k] === 'boolean') this[k] = config[k];
117
109
  if (typeof config.consecutiveToolFailureLimit === 'number') this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
118
110
  if (typeof config.maxTokenBudget === 'number') this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
119
111
  if (typeof config.budgetWarningThreshold === 'number') this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
120
- if (typeof config.autoCheckpointOnMilestone === 'boolean') this.autoCheckpointOnMilestone = config.autoCheckpointOnMilestone;
121
- if (typeof config.enableLiveActivityFeed === 'boolean') this.enableLiveActivityFeed = config.enableLiveActivityFeed;
122
- if (typeof config.enablePreplanning === 'boolean') this.enablePreplanning = config.enablePreplanning;
123
- if (typeof config.autoBranchOnGoalStart === 'boolean') this.autoBranchOnGoalStart = config.autoBranchOnGoalStart;
124
- if (typeof config.enablePostGoalRetrospective === 'boolean') this.enablePostGoalRetrospective = config.enablePostGoalRetrospective;
125
- if (typeof config.autoScaleBudgetNearCompletion === 'boolean') this.autoScaleBudgetNearCompletion = config.autoScaleBudgetNearCompletion;
126
- if (typeof config.enableTemplatesDrawer === 'boolean') this.enableTemplatesDrawer = config.enableTemplatesDrawer;
127
- if (typeof config.enableMilestoneDependencies === 'boolean') this.enableMilestoneDependencies = config.enableMilestoneDependencies;
128
112
  if (typeof config.soundScheme === 'string') this.soundScheme = config.soundScheme;
129
- if (typeof config.enableVoiceAnnouncements === 'boolean') this.enableVoiceAnnouncements = config.enableVoiceAnnouncements;
130
113
  this.emit();
131
114
  }
132
115
 
@@ -140,14 +123,8 @@ export class GoalEngine {
140
123
  this.toolFailureCounters.set(sid, next);
141
124
  return next;
142
125
  }
143
-
144
- resetToolFailureCount(sessionId = 'default') {
145
- this.toolFailureCounters.set(sessionId || 'default', 0);
146
- }
147
-
148
- getToolFailureCount(sessionId = 'default') {
149
- return this.toolFailureCounters.get(sessionId || 'default') || 0;
150
- }
126
+ resetToolFailureCount(sessionId = 'default') { this.toolFailureCounters.set(sessionId || 'default', 0); }
127
+ getToolFailureCount(sessionId = 'default') { return this.toolFailureCounters.get(sessionId || 'default') || 0; }
151
128
 
152
129
  nudge(text, sessionId = 'default') {
153
130
  const sid = sessionId || 'default';
@@ -205,7 +182,7 @@ export class GoalEngine {
205
182
  title: cleanTitle,
206
183
  description: options.description?.trim() || '',
207
184
  lang: options.lang || detectLanguage(cleanTitle),
208
- cwd: options.cwd ? getSafeCwd(options.cwd) : null,
185
+ cwd: options.cwd ? getSafeCwd(options.cwd, options.allowedRoots || this.allowedRoots || []) : null,
209
186
  mode: initialMode,
210
187
  state: initialState,
211
188
  startedAt: now,
@@ -218,7 +195,7 @@ export class GoalEngine {
218
195
  budgetWarningTriggered: false,
219
196
  budgetAutoScaled: false,
220
197
  gitStartCommit: gitCommit || null,
221
- gitBranch: null,
198
+ gitBranch: options.gitBranch || options.branchName || null,
222
199
  gitParentBranch: null,
223
200
  pendingNudge: null,
224
201
  nudges: [],
@@ -229,6 +206,8 @@ export class GoalEngine {
229
206
  preplan: null,
230
207
  retrospective: null,
231
208
  issueRef: parseIssueRef(cleanTitle),
209
+ operatorQuery: null,
210
+ waitingForOperator: false,
232
211
  tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
233
212
  logs: [{
234
213
  timestamp: now,
@@ -338,7 +317,8 @@ export class GoalEngine {
338
317
  }
339
318
 
340
319
  addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
341
- const sid = sessionId || 'default';
320
+ const sid = typeof shouldEmit === 'string' ? shouldEmit : (sessionId || 'default');
321
+ const shouldEmitFlag = typeof shouldEmit === 'boolean' ? shouldEmit : true;
342
322
  const goal = this.goals.get(sid);
343
323
  if (!goal || !Array.isArray(milestonesList)) return [];
344
324
 
@@ -348,7 +328,7 @@ export class GoalEngine {
348
328
  }
349
329
  this.recordProgress(sid);
350
330
  recordActivity(goal, { type: 'milestone', tool: 'addMilestones', status: 'success', details: `Added ${newMilestones.length} milestones` });
351
- if (shouldEmit) this.emit(sid);
331
+ if (shouldEmitFlag) this.emit(sid);
352
332
  return newMilestones;
353
333
  }
354
334
 
@@ -382,6 +362,7 @@ export class GoalEngine {
382
362
 
383
363
  const prevStatus = res.prevStatus;
384
364
  recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'success', details: `${id} -> ${status}` });
365
+ if (this.enableKanbanSync) syncMilestoneStatusToKanban(target, { goal, ctx: this.ctx, store: this.kanbanStore, kanbanService: this.kanbanService }).catch(() => {});
385
366
 
386
367
  if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED) {
387
368
  if (this.autoCheckpointOnMilestone) {
@@ -584,6 +565,28 @@ export class GoalEngine {
584
565
  return this.goals.get(sessionId || 'default') || null;
585
566
  }
586
567
 
568
+ askOperator(query = {}, sessionId = 'default') {
569
+ const sid = sessionId || 'default';
570
+ const goal = this.goals.get(sid);
571
+ if (!goal) return { ok: false, error: 'Goal not found' };
572
+ const res = askOperatorQuery(goal, query);
573
+ this.emit(sid, true);
574
+ return res;
575
+ }
576
+
577
+ answerOperator(payload = {}, sessionId = 'default') {
578
+ const sid = sessionId || 'default';
579
+ const goal = this.goals.get(sid);
580
+ if (!goal) return { ok: false, error: 'Goal not found' };
581
+ const res = answerOperatorQuery(goal, payload);
582
+ this.emit(sid, true);
583
+ return res;
584
+ }
585
+
586
+ getDiff(sessionId = 'default', options = {}) { return getEngineGoalDiff(this, sessionId, options); }
587
+ syncKanban(sessionId = 'default', options = {}) { return syncGoalToKanban(this, sessionId, options); }
588
+ handleKanbanCardMove(payload = {}, sessionId = 'default') { return handleKanbanCardMoved(this, payload, sessionId); }
589
+
587
590
  isPlanMode(sessionId = 'default') {
588
591
  const goal = this.goals.get(sessionId || 'default');
589
592
  return Boolean(goal && (goal.mode === GoalMode.PLAN || goal.state === GoalState.PLANNING));
package/lib/routes.js CHANGED
@@ -383,6 +383,28 @@ export function registerRoutes(ctx, {
383
383
  }));
384
384
  }
385
385
 
386
+ // GET /dsh-goal/diff
387
+ if (req.method === 'GET' && (pathname === '/dsh-goal/diff' || pathname === '/dsh-goal/diff/')) {
388
+ const rejection = getRequestRejection(req, { ctx });
389
+ if (rejection !== undefined) {
390
+ res.statusCode = rejection;
391
+ return res.end(JSON.stringify({ error: rejection === 401 ? 'Unauthorized: authentication required' : 'Forbidden: untrusted caller origin' }));
392
+ }
393
+ const sid = sessionIdOf(req, 'default');
394
+ const target = url.searchParams.get('target') || undefined;
395
+ const base = url.searchParams.get('base') || undefined;
396
+ const file = url.searchParams.get('file') || undefined;
397
+ const cwd = url.searchParams.get('cwd') || undefined;
398
+
399
+ if (engine) {
400
+ const diffResult = engine.getDiff(sid, { target, base, file, cwd });
401
+ res.statusCode = diffResult.ok ? 200 : 400;
402
+ return res.end(JSON.stringify(diffResult));
403
+ }
404
+ res.statusCode = 404;
405
+ return res.end(JSON.stringify({ ok: false, error: 'Goal engine unavailable' }));
406
+ }
407
+
386
408
  // POST /dsh-goal/action
387
409
  // CSRF & Same-Origin guard
388
410
  if (req.method === 'POST' && (pathname === '/dsh-goal/action' || pathname === '/dsh-goal/action/')) {
@@ -429,7 +451,7 @@ export function registerRoutes(ctx, {
429
451
  body += chunk;
430
452
  };
431
453
 
432
- const onEnd = () => {
454
+ const onEnd = async () => {
433
455
  if (limitExceeded) return;
434
456
  try {
435
457
  const data = JSON.parse(body || '{}');
@@ -750,6 +772,30 @@ export function registerRoutes(ctx, {
750
772
  break;
751
773
  }
752
774
 
775
+ case 'answer_operator': {
776
+ const answer = typeof data.answer === 'string' ? data.answer.trim() : '';
777
+ const optionIndex = typeof data.optionIndex === 'number' ? data.optionIndex : (typeof data.option_index === 'number' ? data.option_index : null);
778
+ if (engine && typeof engine.answerOperator === 'function') {
779
+ const ansRes = engine.answerOperator({ answer, optionIndex }, sid);
780
+ if (!ansRes.ok) {
781
+ res.statusCode = 400;
782
+ return res.end(JSON.stringify({ error: ansRes.error || 'Failed to submit operator answer' }));
783
+ }
784
+ result = { ok: true, answer: ansRes.answer, state: engine.getSnapshot(sid) };
785
+ const snap = engine.getSnapshot(sid);
786
+ const detectedLang = snap?.lang || 'en';
787
+ const prompt = detectedLang === 'zh'
788
+ ? `✅ 操作员已答复你的提问: "${ansRes.answer}"。请继续执行任务。`
789
+ : `✅ Operator answered your query: "${ansRes.answer}". Proceed with task execution.`;
790
+ if (typeof resumeActiveAgent === 'function') {
791
+ resumeActiveAgent(prompt, sid);
792
+ }
793
+ } else {
794
+ result = { ok: true, answer };
795
+ }
796
+ break;
797
+ }
798
+
753
799
  case 'approve_plan': {
754
800
  if (engine) {
755
801
  const snap = engine.getSnapshot(sid);
@@ -805,6 +851,28 @@ export function registerRoutes(ctx, {
805
851
  res.statusCode = 200;
806
852
  return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
807
853
 
854
+ case 'kanban_move': {
855
+ if (engine) {
856
+ result = engine.handleKanbanCardMove(data, sid);
857
+ if (!result.ok) res.statusCode = 400;
858
+ } else {
859
+ res.statusCode = 404;
860
+ result = { ok: false, error: 'Goal engine unavailable' };
861
+ }
862
+ break;
863
+ }
864
+
865
+ case 'kanban_sync': {
866
+ if (engine) {
867
+ result = await engine.syncKanban(sid, { ctx });
868
+ if (!result.ok && !result.skipped) res.statusCode = 400;
869
+ } else {
870
+ res.statusCode = 404;
871
+ result = { ok: false, error: 'Goal engine unavailable' };
872
+ }
873
+ break;
874
+ }
875
+
808
876
  case 'instantiate_template': {
809
877
  const tplId = data.templateId || data.id;
810
878
  if (!tplId) {
@@ -846,6 +914,7 @@ export function registerRoutes(ctx, {
846
914
  '/dsh-goal/events': ['GET'],
847
915
  '/dsh-goal/templates': ['GET'],
848
916
  '/dsh-goal/state': ['GET'],
917
+ '/dsh-goal/diff': ['GET'],
849
918
  '/dsh-goal/action': ['POST'],
850
919
  };
851
920
  const cleanPath = pathname.replace(/\/+$/, '') || '/';
package/lib/tools.js CHANGED
@@ -311,5 +311,70 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
311
311
  output: JSON_OUTPUT,
312
312
  execute: handleGoalFinish,
313
313
  });
314
+
315
+ // Tool 6: goal_ask_operator (#166)
316
+ const handleGoalAskOperator = async (args = {}, toolCtx = {}) => {
317
+ const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
318
+ const question = args.question || args.prompt;
319
+ if (!question || typeof question !== 'string' || !question.trim()) {
320
+ throw new Error('Field "question" is required and must be a non-empty string');
321
+ }
322
+
323
+ const options = Array.isArray(args.options) ? args.options : [];
324
+ const isBlocking = args.isBlocking !== false && args.is_blocking !== false;
325
+
326
+ const engine = ctx.goalEngine || (ctx.get && ctx.get('goalEngine'));
327
+ if (engine && typeof engine.askOperator === 'function') {
328
+ const res = engine.askOperator({ question, options, isBlocking }, goalId);
329
+ if (!res.ok) {
330
+ throw new Error(`Failed to present question to operator: ${res.error}`);
331
+ }
332
+ return {
333
+ ok: true,
334
+ goalId,
335
+ queryId: res.queryId,
336
+ question: res.question,
337
+ options: res.options,
338
+ isBlocking: res.isBlocking,
339
+ message: isBlocking
340
+ ? 'Question submitted to human operator. Goal is paused awaiting operator input.'
341
+ : 'Question submitted to human operator in non-blocking mode.',
342
+ };
343
+ }
344
+
345
+ return {
346
+ ok: true,
347
+ goalId,
348
+ question,
349
+ options,
350
+ message: 'Question logged.',
351
+ };
352
+ };
353
+
354
+ safeRegister({
355
+ name: 'goal_ask_operator',
356
+ description: 'Ask the human operator a clarification question or present architectural choices when encountering blockers (#166). Pauses autonomous execution without consuming iterations until the operator responds.',
357
+ parameters: {
358
+ type: 'object',
359
+ properties: {
360
+ question: {
361
+ type: 'string',
362
+ description: 'The clarification question, design decision, or blocker to ask the human operator',
363
+ },
364
+ options: {
365
+ type: 'array',
366
+ items: { type: 'string' },
367
+ description: 'Optional list of predefined choices or alternative options for the operator to select with one click',
368
+ },
369
+ is_blocking: {
370
+ type: 'boolean',
371
+ description: 'Whether to pause goal execution awaiting operator response (default: true)',
372
+ },
373
+ },
374
+ required: ['question'],
375
+ },
376
+ output: JSON_OUTPUT,
377
+ execute: handleGoalAskOperator,
378
+ });
314
379
  });
315
380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Core Goal Addon (Token Budget, Milestones, Git Snapshots & Retrospective Reports) for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",