@goodandready/dsh-goal 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -22,6 +22,15 @@ export const Config = z.object({
22
22
  maxTokenBudget: z.number().default(0).description('Maximum token budget per goal session (0 to disable)'),
23
23
  budgetWarningThreshold: z.number().default(80).description('Percentage of token budget consumed before model warning injection (e.g. 80)'),
24
24
  autoCheckpointOnMilestone: z.boolean().default(false).description('Automatically create git commit checkpoint upon milestone completion'),
25
+ enableLiveActivityFeed: z.boolean().default(true).description('Display compact recent tool and control activity feed in details modal'),
26
+ enablePreplanning: z.boolean().default(true).description('Generate initial draft plan for approval before launching autonomous turns'),
27
+ autoBranchOnGoalStart: z.boolean().default(true).description('Automatically create dedicated isolated git branch for goal execution'),
28
+ enablePostGoalRetrospective: z.boolean().default(true).description('Generate structured retrospective and analytics card upon goal completion'),
29
+ autoScaleBudgetNearCompletion: z.boolean().default(false).description('Automatically grant token buffer extension if budget reaches 100% near completion'),
30
+ enableTemplatesDrawer: z.boolean().default(true).description('Provide pre-configured engineering goal templates in UI drawer'),
31
+ enableMilestoneDependencies: z.boolean().default(true).description('Enforce milestone dependency ordering and block premature execution'),
32
+ soundScheme: z.string().default('default').description('Audio theme for status and milestone notifications'),
33
+ enableVoiceAnnouncements: z.boolean().default(false).description('Use Web Speech API for voice announcements on milestone events'),
25
34
  storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
26
35
  });
27
36
 
@@ -43,6 +52,15 @@ export function apply(ctx, config = {}) {
43
52
  maxTokenBudget: config?.maxTokenBudget ?? 0,
44
53
  budgetWarningThreshold: config?.budgetWarningThreshold ?? 80,
45
54
  autoCheckpointOnMilestone: config?.autoCheckpointOnMilestone ?? false,
55
+ enableLiveActivityFeed: config?.enableLiveActivityFeed ?? true,
56
+ enablePreplanning: config?.enablePreplanning ?? true,
57
+ autoBranchOnGoalStart: config?.autoBranchOnGoalStart ?? true,
58
+ enablePostGoalRetrospective: config?.enablePostGoalRetrospective ?? true,
59
+ autoScaleBudgetNearCompletion: config?.autoScaleBudgetNearCompletion ?? false,
60
+ enableTemplatesDrawer: config?.enableTemplatesDrawer ?? true,
61
+ enableMilestoneDependencies: config?.enableMilestoneDependencies ?? true,
62
+ soundScheme: config?.soundScheme ?? 'default',
63
+ enableVoiceAnnouncements: config?.enableVoiceAnnouncements ?? false,
46
64
  storagePath: config?.storagePath,
47
65
  };
48
66
 
@@ -60,6 +78,15 @@ export function apply(ctx, config = {}) {
60
78
  maxTokenBudget: currentSettings.maxTokenBudget,
61
79
  budgetWarningThreshold: currentSettings.budgetWarningThreshold,
62
80
  autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
81
+ enableLiveActivityFeed: currentSettings.enableLiveActivityFeed,
82
+ enablePreplanning: currentSettings.enablePreplanning,
83
+ autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
84
+ enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
85
+ autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
86
+ enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
87
+ enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
88
+ soundScheme: currentSettings.soundScheme,
89
+ enableVoiceAnnouncements: currentSettings.enableVoiceAnnouncements,
63
90
  storagePath,
64
91
  });
65
92
 
@@ -175,35 +202,19 @@ export function apply(ctx, config = {}) {
175
202
  const getConfig = () => {
176
203
  if (settingsScope) {
177
204
  const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
205
+ let live = null;
178
206
  if (snap) {
179
- if (snap.status === 'ready' || snap.status === undefined) {
180
- const live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
181
- if (live && typeof live === 'object') {
182
- return {
183
- maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
184
- autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
185
- enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
186
- showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
187
- consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
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,
192
- storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
193
- };
194
- }
207
+ if (snap.status === 'ready' || snap.status === 'writable' || snap.status === undefined) {
208
+ live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
195
209
  }
196
210
  } else if (typeof settingsScope.get === 'function') {
197
- const live = settingsScope.get();
198
- if (live && typeof live === 'object') {
199
- return {
200
- maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
201
- autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
202
- enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
203
- showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
204
- storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
205
- };
206
- }
211
+ live = settingsScope.get();
212
+ }
213
+ if (live && typeof live === 'object') {
214
+ return {
215
+ ...currentSettings,
216
+ ...live,
217
+ };
207
218
  }
208
219
  }
209
220
  return currentSettings;
@@ -220,6 +231,15 @@ export function apply(ctx, config = {}) {
220
231
  maxTokenBudget: live.maxTokenBudget,
221
232
  budgetWarningThreshold: live.budgetWarningThreshold,
222
233
  autoCheckpointOnMilestone: live.autoCheckpointOnMilestone,
234
+ enableLiveActivityFeed: live.enableLiveActivityFeed,
235
+ enablePreplanning: live.enablePreplanning,
236
+ autoBranchOnGoalStart: live.autoBranchOnGoalStart,
237
+ enablePostGoalRetrospective: live.enablePostGoalRetrospective,
238
+ autoScaleBudgetNearCompletion: live.autoScaleBudgetNearCompletion,
239
+ enableTemplatesDrawer: live.enableTemplatesDrawer,
240
+ enableMilestoneDependencies: live.enableMilestoneDependencies,
241
+ soundScheme: live.soundScheme,
242
+ enableVoiceAnnouncements: live.enableVoiceAnnouncements,
223
243
  });
224
244
  };
225
245
 
@@ -280,7 +300,9 @@ export function apply(ctx, config = {}) {
280
300
  try {
281
301
  if (typeof pctx.systemPrompt?.section === 'function') {
282
302
  const globalSections = pctx.systemPrompt.layers?.global?.sections;
303
+ let displacedPromptSection = null;
283
304
  if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
305
+ displacedPromptSection = globalSections.data.get('tool:goal');
284
306
  globalSections.data.delete('tool:goal');
285
307
  }
286
308
 
@@ -298,7 +320,12 @@ export function apply(ctx, config = {}) {
298
320
  });
299
321
 
300
322
  if (typeof unregisterSection === 'function') {
301
- pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
323
+ pctx.effect(() => () => {
324
+ unregisterSection();
325
+ if (displacedPromptSection && globalSections?.data instanceof Map) {
326
+ globalSections.data.set('tool:goal', displacedPromptSection);
327
+ }
328
+ }, 'dsh-goal: system prompt section');
302
329
  }
303
330
  }
304
331
  } catch (err) {
package/lib/routes.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
4
4
  import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
5
+ import { getTemplatesCatalogue } from './engine-templates.js';
5
6
 
6
7
  export function isLoopback(value) {
7
8
  if (!value) return false;
@@ -20,10 +21,6 @@ export function isTrustedCaller(req) {
20
21
  const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || '';
21
22
  if (isLoopback(remote)) return true;
22
23
 
23
- const secSite = String(req.headers?.['sec-fetch-site'] || '').toLowerCase();
24
- if (secSite === 'same-origin' || secSite === 'same-site' || secSite === 'none') return true;
25
- if (secSite === 'cross-site') return false;
26
-
27
24
  const host = String(req.headers?.host || '').toLowerCase();
28
25
  const origin = String(req.headers?.origin || '').trim().toLowerCase();
29
26
  if (origin) {
@@ -166,6 +163,11 @@ export function registerRoutes(ctx, {
166
163
 
167
164
  // GET /dsh-goal/events: Server-Sent Events realtime snapshot stream
168
165
  if (req.method === 'GET' && (pathname === '/dsh-goal/events' || pathname === '/dsh-goal/events/')) {
166
+ if (!isTrustedCaller(req)) {
167
+ res.statusCode = 403;
168
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
169
+ return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
170
+ }
169
171
  const sid = sessionIdOf(req, 'default');
170
172
  res.writeHead(200, {
171
173
  'Content-Type': 'text/event-stream',
@@ -194,8 +196,22 @@ export function registerRoutes(ctx, {
194
196
 
195
197
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
196
198
 
199
+ // GET /dsh-goal/templates
200
+ if (req.method === 'GET' && (pathname === '/dsh-goal/templates' || pathname === '/dsh-goal/templates/')) {
201
+ if (!isTrustedCaller(req)) {
202
+ res.statusCode = 403;
203
+ return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
204
+ }
205
+ res.statusCode = 200;
206
+ return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
207
+ }
208
+
197
209
  // GET /dsh-goal/state
198
210
  if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
211
+ if (!isTrustedCaller(req)) {
212
+ res.statusCode = 403;
213
+ return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
214
+ }
199
215
  const sid = sessionIdOf(req, 'default');
200
216
  res.statusCode = 200;
201
217
  return res.end(JSON.stringify(engine.getSnapshot(sid)));
@@ -261,13 +277,26 @@ export function registerRoutes(ctx, {
261
277
  res.statusCode = 400;
262
278
  return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
263
279
  }
280
+ const trustedRoots = [process.cwd()];
281
+ if (process.env.DSH_WORKSPACE_ROOT) {
282
+ trustedRoots.push(process.env.DSH_WORKSPACE_ROOT);
283
+ }
284
+ let safeStartCwd = undefined;
285
+ if (data.cwd) {
286
+ const validated = getSafeCwd(data.cwd, trustedRoots);
287
+ if (!validated) {
288
+ res.statusCode = 400;
289
+ return res.end(JSON.stringify({ error: 'Invalid or unauthorized working directory' }));
290
+ }
291
+ safeStartCwd = validated;
292
+ }
264
293
  const detectedLang = data.lang || detectLanguage(cleanTitle);
265
294
  const conf = typeof getConfig === 'function' ? getConfig() : { maxIterations: 25 };
266
295
  result = engine.startGoal(cleanTitle, {
267
296
  description: typeof description === 'string' ? description.trim() : '',
268
297
  maxIterations: conf.maxIterations || 25,
269
298
  lang: detectedLang,
270
- cwd: data.cwd,
299
+ cwd: safeStartCwd,
271
300
  }, sid);
272
301
 
273
302
  const startPrompt = detectedLang === 'zh'
@@ -383,7 +412,15 @@ export function registerRoutes(ctx, {
383
412
  }
384
413
  const sessionGoal = engine.getGoal(sid);
385
414
  const allowedRoots = [process.cwd()];
386
- if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
415
+ if (process.env.DSH_WORKSPACE_ROOT) {
416
+ allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
417
+ }
418
+ if (sessionGoal?.cwd) {
419
+ const isCwdAllowed = allowedRoots.some((root) => isPathInsideOrEqual(root, sessionGoal.cwd));
420
+ if (isCwdAllowed) {
421
+ allowedRoots.push(sessionGoal.cwd);
422
+ }
423
+ }
387
424
  const safeCwd = getSafeCwd(data.cwd, allowedRoots);
388
425
  if (!safeCwd) {
389
426
  res.statusCode = 400;
@@ -406,7 +443,15 @@ export function registerRoutes(ctx, {
406
443
  }
407
444
  const sessionGoal = engine.getGoal(sid);
408
445
  const allowedRoots = [process.cwd()];
409
- if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
446
+ if (process.env.DSH_WORKSPACE_ROOT) {
447
+ allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
448
+ }
449
+ if (sessionGoal?.cwd) {
450
+ const isCwdAllowed = allowedRoots.some((root) => isPathInsideOrEqual(root, sessionGoal.cwd));
451
+ if (isCwdAllowed) {
452
+ allowedRoots.push(sessionGoal.cwd);
453
+ }
454
+ }
410
455
  const safeCwd = getSafeCwd(data.cwd, allowedRoots);
411
456
  if (!safeCwd) {
412
457
  res.statusCode = 400;
@@ -421,6 +466,51 @@ export function registerRoutes(ctx, {
421
466
  return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
422
467
  }
423
468
 
469
+ case 'intervene': {
470
+ result = engine.intervene(data, sid);
471
+ if (data.resume) {
472
+ engine.resume(sid);
473
+ const prompt = engine.getStatePromptInjection(sid);
474
+ if (typeof resumeActiveAgent === 'function') {
475
+ resumeActiveAgent(prompt, sid);
476
+ }
477
+ }
478
+ break;
479
+ }
480
+
481
+ case 'preplan_approve': {
482
+ result = engine.approvePreplan(sid);
483
+ const prompt = engine.getStatePromptInjection(sid);
484
+ if (typeof resumeActiveAgent === 'function') {
485
+ resumeActiveAgent(prompt, sid);
486
+ }
487
+ break;
488
+ }
489
+
490
+ case 'branch_action': {
491
+ const op = String(data.operation || data.branchOperation || 'merge').toLowerCase();
492
+ const targetBranch = String(data.targetBranch || 'main');
493
+ if (op === 'merge') {
494
+ const ok = engine.mergeGoalBranch(targetBranch, sid);
495
+ if (!ok) {
496
+ res.statusCode = 500;
497
+ return res.end(JSON.stringify({ error: `Failed to merge goal branch into ${targetBranch}` }));
498
+ }
499
+ } else if (op === 'discard') {
500
+ const ok = engine.discardGoalBranch(targetBranch, sid);
501
+ if (!ok) {
502
+ res.statusCode = 500;
503
+ return res.end(JSON.stringify({ error: 'Failed to discard goal branch' }));
504
+ }
505
+ }
506
+ result = engine.getSnapshot(sid);
507
+ break;
508
+ }
509
+
510
+ case 'templates':
511
+ res.statusCode = 200;
512
+ return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
513
+
424
514
  default:
425
515
  res.statusCode = 400;
426
516
  return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
package/lib/tools.js CHANGED
@@ -46,16 +46,23 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
46
46
  ctx.inject(['tools'], (tctx) => {
47
47
  if (!tctx.tools?.register) return;
48
48
 
49
+ const displacedTools = new Map();
49
50
  const safeRegister = (definition) => {
50
51
  try {
51
52
  const name = definition.name;
52
53
  const globalTools = tctx.tools.layers?.global?.tools;
53
54
  if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
55
+ displacedTools.set(name, globalTools.data.get(name));
54
56
  globalTools.data.delete(name);
55
57
  }
56
58
  const unregister = tctx.tools.register(definition);
57
59
  if (typeof unregister === 'function') {
58
- tctx.effect(() => () => unregister(), `dsh-goal: tool ${name}`);
60
+ tctx.effect(() => () => {
61
+ unregister();
62
+ if (displacedTools.has(name) && globalTools?.data instanceof Map) {
63
+ globalTools.data.set(name, displacedTools.get(name));
64
+ }
65
+ }, `dsh-goal: tool ${name}`);
59
66
  }
60
67
  } catch (err) {
61
68
  console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
@@ -195,6 +202,9 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
195
202
  const handleSetMilestones = async ({ milestones }, toolCtx) => {
196
203
  const sid = sessionIdOf(toolCtx, 'default');
197
204
  const snap = engine.setMilestones(milestones, sid);
205
+ if (typeof engine.recordToolActivity === 'function') {
206
+ engine.recordToolActivity('goal_set_milestones', 'success', 'Defined milestones', sid);
207
+ }
198
208
  return {
199
209
  success: true,
200
210
  count: snap.milestones?.length || 0,
@@ -215,6 +225,7 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
215
225
  properties: {
216
226
  id: { type: 'string', description: 'Short identifier, e.g. M1, step-1' },
217
227
  title: { type: 'string', description: 'Concise actionable title' },
228
+ dependsOn: { type: 'array', items: { type: 'string' }, description: 'Prerequisite milestone IDs' },
218
229
  },
219
230
  required: ['id', 'title'],
220
231
  },
@@ -231,7 +242,10 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
231
242
  // Tool 5: goal_update_progress
232
243
  const handleUpdateProgress = async ({ milestone_id, status, notes, checklist }, toolCtx) => {
233
244
  const sid = sessionIdOf(toolCtx, 'default');
234
- engine.updateMilestone(milestone_id, status, notes, sid, checklist);
245
+ const ok = engine.updateMilestone(milestone_id, status, notes, sid, checklist);
246
+ if (typeof engine.recordToolActivity === 'function') {
247
+ engine.recordToolActivity('goal_update_progress', ok ? 'success' : 'error', String(milestone_id) + ' -> ' + String(status), sid);
248
+ }
235
249
  const snap = engine.getSnapshot(sid);
236
250
  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, ''));
237
251
  return {
@@ -282,6 +296,9 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
282
296
  const handleGoalFinish = async ({ summary }, toolCtx) => {
283
297
  const sid = sessionIdOf(toolCtx, 'default');
284
298
  const snap = engine.completeGoal(summary, sid);
299
+ if (typeof engine.recordToolActivity === 'function') {
300
+ engine.recordToolActivity('goal_finish', 'success', summary ? summary.slice(0, 80) : 'Goal finished', sid);
301
+ }
285
302
  return {
286
303
  success: true,
287
304
  completed: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",