@goodandready/dsh-goal 0.2.11 → 0.2.12

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/client.js CHANGED
@@ -1609,7 +1609,7 @@ window.__ModuleLoader__.load({
1609
1609
  if (!AudioCtx) return;
1610
1610
  const actx = new AudioCtx();
1611
1611
  if (actx.state === 'suspended') {
1612
- actx.resume().catch(() => {});
1612
+ actx.resume().catch((err) => { console.warn('[dsh-goal] Fetch templates error:', err); });
1613
1613
  }
1614
1614
  const now = actx.currentTime;
1615
1615
  if (scheme === 'retro') {
@@ -2590,9 +2590,51 @@ window.__ModuleLoader__.load({
2590
2590
  function QuickLaunchModal({ onClose, onStart, t }) {
2591
2591
 
2592
2592
  const [title, setTitle] = useState('');
2593
+ const [showTemplates, setShowTemplates] = useState(false);
2594
+ const [backendTemplates, setBackendTemplates] = useState([]);
2595
+ const [selectedTpl, setSelectedTpl] = useState(null);
2596
+ const [tplInputs, setTplInputs] = useState({});
2597
+ const [loadingTpls, setLoadingTpls] = useState(false);
2593
2598
 
2594
2599
  const inputRef = useRef(null);
2595
2600
 
2601
+ useEffect(() => {
2602
+ if (showTemplates && backendTemplates.length === 0) {
2603
+ setLoadingTpls(true);
2604
+ fetch('/dsh-goal/templates')
2605
+ .then((r) => r.json())
2606
+ .then((d) => {
2607
+ if (d && Array.isArray(d.templates)) {
2608
+ setBackendTemplates(d.templates);
2609
+ }
2610
+ })
2611
+ .catch((err) => { console.warn('[dsh-goal] Fetch templates error:', err); })
2612
+ .finally(() => setLoadingTpls(false));
2613
+ }
2614
+ }, [showTemplates, backendTemplates.length]);
2615
+
2616
+ const handleApplyTemplate = async () => {
2617
+ if (!selectedTpl) return;
2618
+ try {
2619
+ const res = await fetch('/dsh-goal/action', {
2620
+ method: 'POST',
2621
+ headers: { 'Content-Type': 'application/json' },
2622
+ body: JSON.stringify({
2623
+ action: 'instantiate_template',
2624
+ templateId: selectedTpl.id,
2625
+ inputs: tplInputs,
2626
+ }),
2627
+ });
2628
+ if (res.ok) {
2629
+ const data = await res.json();
2630
+ if (data.ok && data.template) {
2631
+ setTitle(data.template.title);
2632
+ setShowTemplates(false);
2633
+ }
2634
+ }
2635
+ } catch (err) { console.warn('[dsh-goal] Apply template error:', err); }
2636
+ };
2637
+
2596
2638
  useEffect(() => {
2597
2639
 
2598
2640
  if (inputRef.current) inputRef.current.focus();
@@ -2782,8 +2824,94 @@ window.__ModuleLoader__.load({
2782
2824
  },
2783
2825
  tpl.label
2784
2826
  )
2827
+ ),
2828
+ React.createElement(
2829
+ 'button',
2830
+ {
2831
+ type: 'button',
2832
+ className: 'dsh-goal-chip-btn',
2833
+ style: { fontWeight: 600, borderColor: 'var(--dsw-alias-border-brand, var(--dsw-alias-border-l2))' },
2834
+ onClick: () => setShowTemplates(!showTemplates),
2835
+ },
2836
+ (showTemplates ? '▲ ' : '▼ ') + '📋 ' + (t('templatesTitle') || 'Engineering Templates')
2785
2837
  )
2786
2838
  ),
2839
+ showTemplates
2840
+ ? React.createElement(
2841
+ 'div',
2842
+ {
2843
+ style: {
2844
+ marginTop: 10,
2845
+ padding: '10px 12px',
2846
+ borderRadius: 6,
2847
+ border: '1px solid var(--dsw-alias-border-l1)',
2848
+ background: 'var(--dsw-alias-bg-hover)',
2849
+ },
2850
+ },
2851
+ loadingTpls
2852
+ ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading templates…')
2853
+ : React.createElement(
2854
+ 'div',
2855
+ null,
2856
+ React.createElement(
2857
+ 'div',
2858
+ { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 } },
2859
+ backendTemplates.map((bt) =>
2860
+ React.createElement(
2861
+ 'button',
2862
+ {
2863
+ key: bt.id,
2864
+ type: 'button',
2865
+ className: `dsh-goal-chip-btn ${selectedTpl?.id === bt.id ? 'active' : ''}`,
2866
+ style: selectedTpl?.id === bt.id ? { fontWeight: 600, border: '1px solid var(--dsw-alias-accent-primary)' } : {},
2867
+ onClick: () => {
2868
+ setSelectedTpl(bt);
2869
+ const initial = {};
2870
+ (bt.inputs || []).forEach((inp) => {
2871
+ initial[inp.key] = inp.defaultValue || '';
2872
+ });
2873
+ setTplInputs(initial);
2874
+ },
2875
+ },
2876
+ bt.title
2877
+ )
2878
+ )
2879
+ ),
2880
+ selectedTpl
2881
+ ? React.createElement(
2882
+ 'div',
2883
+ { style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: 8 } },
2884
+ React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: 6 } }, selectedTpl.description),
2885
+ (selectedTpl.inputs || []).map((inp) =>
2886
+ React.createElement(
2887
+ 'div',
2888
+ { key: inp.key, style: { marginBottom: 6 } },
2889
+ React.createElement('label', { style: { fontSize: 11, display: 'block', marginBottom: 2 } }, inp.label),
2890
+ React.createElement('input', {
2891
+ type: 'text',
2892
+ className: 'dsh-goal-card-input',
2893
+ style: { width: '100%', boxSizing: 'border-box', fontSize: 12, padding: '4px 8px' },
2894
+ placeholder: inp.placeholder || '',
2895
+ value: tplInputs[inp.key] || '',
2896
+ onChange: (e) => setTplInputs({ ...tplInputs, [inp.key]: e.target.value }),
2897
+ })
2898
+ )
2899
+ ),
2900
+ React.createElement(
2901
+ 'button',
2902
+ {
2903
+ type: 'button',
2904
+ className: 'dsh-goal-btn dsh-goal-btn-primary',
2905
+ style: { marginTop: 4, padding: '4px 10px', fontSize: 12 },
2906
+ onClick: handleApplyTemplate,
2907
+ },
2908
+ 'Apply Template'
2909
+ )
2910
+ )
2911
+ : null
2912
+ )
2913
+ )
2914
+ : null
2787
2915
  ),
2788
2916
 
2789
2917
  React.createElement(
@@ -3398,6 +3526,17 @@ window.__ModuleLoader__.load({
3398
3526
 
3399
3527
  const isCompleted = state.state === 'COMPLETED';
3400
3528
 
3529
+ const formatElapsed = (totalSeconds) => {
3530
+ const s = Math.max(0, Math.floor(Number(totalSeconds) || 0));
3531
+ if (s < 60) return `${s}s`;
3532
+ const m = Math.floor(s / 60);
3533
+ const remS = s % 60;
3534
+ if (m < 60) return remS > 0 ? `${m}m ${remS}s` : `${m}m`;
3535
+ const h = Math.floor(m / 60);
3536
+ const remM = m % 60;
3537
+ return remM > 0 ? `${h}h ${remM}m` : `${h}h`;
3538
+ };
3539
+
3401
3540
  const formatLiveElapsed = () => {
3402
3541
 
3403
3542
  if (!state) return '0s';
@@ -3821,7 +3960,7 @@ window.__ModuleLoader__.load({
3821
3960
  const [updateState, setUpdateState] = useState({
3822
3961
  checking: false,
3823
3962
  updating: false,
3824
- currentVersion: '0.2.3',
3963
+ currentVersion: '',
3825
3964
  latestVersion: '',
3826
3965
  updateAvailable: false,
3827
3966
  notice: '',
@@ -4159,7 +4298,7 @@ window.__ModuleLoader__.load({
4159
4298
  'div',
4160
4299
  { style: { display: 'flex', alignItems: 'center', gap: 8 } },
4161
4300
  React.createElement('span', { style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } }, t('version') || 'Version'),
4162
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'v' + (updateState.currentVersion || '0.2.3')),
4301
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, updateState.currentVersion ? 'v' + updateState.currentVersion : 'v0.2.12'),
4163
4302
  updateState.updateAvailable
4164
4303
  ? React.createElement('span', { className: 'dsh-goal-badge dsh-goal-badge-warn', style: { fontSize: 11 } }, 'v' + updateState.latestVersion + ' ' + (t('updateAvailable') || 'available'))
4165
4304
  : React.createElement('span', { className: 'dsh-goal-badge dsh-goal-badge-ok', style: { fontSize: 11 } }, '✓ ' + (t('upToDate') || 'Up to date')),
@@ -6,9 +6,10 @@ import { GoalState } from './goal-engine-constants.js';
6
6
  * Storage manager for goal engine disk state persistence
7
7
  */
8
8
  export class EngineStore {
9
- constructor(engine, storagePath) {
9
+ constructor(engine, storagePath, logger) {
10
10
  this.engine = engine;
11
11
  this.storagePath = storagePath || null;
12
+ this.logger = logger || engine?.logger || console;
12
13
  this.saveTimer = null;
13
14
  }
14
15
 
@@ -77,7 +78,7 @@ export class EngineStore {
77
78
  }
78
79
  } catch (err) {
79
80
  // best-effort load failure logged defensively
80
- console.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
81
+ this.logger?.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
81
82
  }
82
83
  }
83
84
 
@@ -128,7 +129,7 @@ export class EngineStore {
128
129
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
129
130
  fs.renameSync(tmp, this.storagePath);
130
131
  } catch (err) {
131
- console.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
132
+ this.logger?.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
132
133
  }
133
134
  }
134
135
 
@@ -5,12 +5,12 @@ import { buildStatePromptInjection } from './engine-prompt.js';
5
5
  import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
6
6
  import { applyIntervention, recordActivity, processTokenBudget } from './engine-intervention.js';
7
7
  import { createGoalBranch, sanitizeBranchSlug, getCurrentGitBranch, mergeGoalBranch, discardGoalBranch } from './engine-git-branch.js';
8
- import { parseIssueRef, syncMilestoneWithIssue } from './engine-issue-sync.js';
8
+ import { parseIssueRef, parseIssueChecklist, syncMilestoneWithIssue } from './engine-issue-sync.js';
9
9
  import { generateRetrospectiveData } from './engine-retrospective.js';
10
10
  import { generatePreplanDraft } from './engine-templates.js';
11
11
  import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget } from './engine-sessions.js';
12
12
 
13
- export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone };
13
+ export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist };
14
14
  export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
15
15
 
16
16
  /**
@@ -73,7 +73,7 @@ export class GoalEngine {
73
73
  try {
74
74
  listener(snapshot, sid);
75
75
  } catch (err) {
76
- console.error('[dsh-goal] Error in GoalEngine listener:', err);
76
+ this.logger?.error?.('[dsh-goal] Error in GoalEngine listener:', err);
77
77
  }
78
78
  }
79
79
  }
@@ -234,6 +234,13 @@ export class GoalEngine {
234
234
  }
235
235
  }
236
236
 
237
+ if (options.issueBody && (!options.milestones || options.milestones.length === 0)) {
238
+ const items = parseIssueChecklist(options.issueBody);
239
+ if (items.length > 0) {
240
+ options.milestones = items.map((it) => ({ title: it.text, status: it.done ? MilestoneStatus.COMPLETED : MilestoneStatus.PENDING }));
241
+ }
242
+ }
243
+
237
244
  if (this.enablePreplanning && options.preplan !== false && (!options.milestones || options.milestones.length === 0)) {
238
245
  goal.preplan = { active: true, draftMilestones: generatePreplanDraft(cleanTitle) };
239
246
  }
package/lib/index.js CHANGED
@@ -65,9 +65,12 @@ export function apply(ctx, config = {}) {
65
65
  };
66
66
 
67
67
  const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
68
- const storagePath = currentSettings.storagePath !== undefined && currentSettings.storagePath !== null
69
- ? currentSettings.storagePath
70
- : path.join(defaultStorageDir, 'dsh-goal-state.json');
68
+ const customStorage = typeof currentSettings.storagePath === 'string' && currentSettings.storagePath.trim()
69
+ ? currentSettings.storagePath.trim()
70
+ : null;
71
+ const isTest = process.env.NODE_ENV === 'test' || Boolean(process.env.NODE_TEST_CONTEXT);
72
+ const storagePath = customStorage || (isTest ? null : path.join(defaultStorageDir, 'dsh-goal-state.json'));
73
+ const logger = ctx.logger ? ctx.logger('goal') : console;
71
74
 
72
75
  const engine = new GoalEngine({
73
76
  defaultMaxIterations: currentSettings.maxIterations,
@@ -88,6 +91,7 @@ export function apply(ctx, config = {}) {
88
91
  soundScheme: currentSettings.soundScheme,
89
92
  enableVoiceAnnouncements: currentSettings.enableVoiceAnnouncements,
90
93
  storagePath,
94
+ logger,
91
95
  });
92
96
 
93
97
  // Track running agents through global DSH core event
@@ -122,7 +126,7 @@ export function apply(ctx, config = {}) {
122
126
  try {
123
127
  specificAgent.cancel({ kind: 'user' });
124
128
  } catch (err) {
125
- console.warn('[dsh-goal] Failed to cancel specific agent for session:', err);
129
+ logger.warn('Failed to cancel specific agent for session:', err);
126
130
  }
127
131
  }
128
132
 
@@ -135,7 +139,7 @@ export function apply(ctx, config = {}) {
135
139
  }
136
140
  }
137
141
  } catch (err) {
138
- console.warn('[dsh-goal] Failed to cancel running agent:', err);
142
+ logger.warn('Failed to cancel running agent:', err);
139
143
  }
140
144
  }
141
145
 
@@ -145,7 +149,7 @@ export function apply(ctx, config = {}) {
145
149
  try {
146
150
  lastActiveAgent.cancel({ kind: 'user' });
147
151
  } catch (err) {
148
- console.warn('[dsh-goal] Failed to cancel lastActiveAgent:', err);
152
+ logger.warn('Failed to cancel lastActiveAgent:', err);
149
153
  }
150
154
  }
151
155
  }
@@ -193,7 +197,7 @@ export function apply(ctx, config = {}) {
193
197
  sessionAgents.set(sid, target);
194
198
  return true;
195
199
  } catch (err) {
196
- console.warn('[dsh-goal] Failed to resume agent:', err);
200
+ logger.warn('Failed to resume agent:', err);
197
201
  }
198
202
  }
199
203
  return false;
@@ -263,7 +267,7 @@ export function apply(ctx, config = {}) {
263
267
  }
264
268
  }
265
269
  } catch (err) {
266
- console.warn('[dsh-goal] Settings register skipped:', err.message);
270
+ logger.warn('Settings register skipped:', err.message);
267
271
  }
268
272
  });
269
273
 
@@ -291,7 +295,7 @@ export function apply(ctx, config = {}) {
291
295
  cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
292
296
  }
293
297
  } catch (err) {
294
- console.warn('[dsh-goal] Commands register skipped:', err.message);
298
+ logger.warn('Commands register skipped:', err.message);
295
299
  }
296
300
  });
297
301
 
@@ -329,24 +333,30 @@ export function apply(ctx, config = {}) {
329
333
  }
330
334
  }
331
335
  } catch (err) {
332
- console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
336
+ logger.warn('SystemPrompt section register skipped:', err.message);
333
337
  }
334
338
  });
335
339
 
336
340
  // 4. Tools registration
337
- registerTools(ctx, { engine, resumeActiveAgent });
341
+ registerTools(ctx, { engine, resumeActiveAgent, logger });
338
342
 
339
- // 5. One-click auto-updater registration
343
+ // 5. One-click auto-updater registration with Cordis effect cleanup (Issue #99)
340
344
  if (ctx.webServer?.register) {
341
- try {
342
- registerPluginUpdater(ctx, {
343
- packageName: '@goodandready/dsh-goal',
344
- endpoint: '/api/dsh-goal/update',
345
- manifestUrl: new URL('../package.json', import.meta.url),
346
- });
347
- } catch (err) {
348
- console.warn('[dsh-goal] Failed to register plugin updater:', err.message);
349
- }
345
+ ctx.effect(() => {
346
+ let unreg = null;
347
+ try {
348
+ unreg = registerPluginUpdater(ctx, {
349
+ packageName: '@goodandready/dsh-goal',
350
+ endpoint: '/api/dsh-goal/update',
351
+ manifestUrl: new URL('../package.json', import.meta.url),
352
+ });
353
+ } catch (err) {
354
+ logger.warn('Failed to register plugin updater:', err.message);
355
+ }
356
+ return () => {
357
+ if (typeof unreg === 'function') unreg();
358
+ };
359
+ }, 'dsh-goal: plugin auto-updater');
350
360
  }
351
361
 
352
362
  // 6. WebServer HTTP & SSE routes
@@ -457,7 +467,7 @@ export function apply(ctx, config = {}) {
457
467
  target.followup(promptMsg);
458
468
  sessionAgents.set(sid, target);
459
469
  } catch (err) {
460
- console.warn('[dsh-goal] Failed auto-drive followup:', err);
470
+ logger.warn('Failed auto-drive followup:', err);
461
471
  }
462
472
  }
463
473
  }
package/lib/routes.js CHANGED
@@ -2,7 +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
+ import { getTemplatesCatalogue, instantiateTemplate } from './engine-templates.js';
6
6
 
7
7
  export function isLoopback(value) {
8
8
  if (!value) return false;
@@ -297,6 +297,7 @@ export function registerRoutes(ctx, {
297
297
  maxIterations: conf.maxIterations || 25,
298
298
  lang: detectedLang,
299
299
  cwd: safeStartCwd,
300
+ issueBody: data.issueBody,
300
301
  }, sid);
301
302
 
302
303
  const startPrompt = detectedLang === 'zh'
@@ -511,6 +512,21 @@ export function registerRoutes(ctx, {
511
512
  res.statusCode = 200;
512
513
  return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
513
514
 
515
+ case 'instantiate_template': {
516
+ const tplId = data.templateId || data.id;
517
+ if (!tplId) {
518
+ res.statusCode = 400;
519
+ return res.end(JSON.stringify({ error: 'templateId is required' }));
520
+ }
521
+ const instance = instantiateTemplate(tplId, data.inputs || {});
522
+ if (!instance) {
523
+ res.statusCode = 404;
524
+ return res.end(JSON.stringify({ error: `Template "${tplId}" not found` }));
525
+ }
526
+ res.statusCode = 200;
527
+ return res.end(JSON.stringify({ ok: true, template: instance }));
528
+ }
529
+
514
530
  default:
515
531
  res.statusCode = 400;
516
532
  return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
@@ -526,6 +542,20 @@ export function registerRoutes(ctx, {
526
542
  return;
527
543
  }
528
544
 
545
+ const KNOWN_ROUTES = {
546
+ '/dsh-goal/events': ['GET'],
547
+ '/dsh-goal/templates': ['GET'],
548
+ '/dsh-goal/state': ['GET'],
549
+ '/dsh-goal/action': ['POST'],
550
+ };
551
+ const cleanPath = pathname.replace(/\/+$/, '') || '/';
552
+ const allowedMethods = KNOWN_ROUTES[cleanPath];
553
+ if (allowedMethods && !allowedMethods.includes(req.method)) {
554
+ res.statusCode = 405;
555
+ res.setHeader('Allow', allowedMethods.join(', '));
556
+ return res.end(JSON.stringify({ error: `Method Not Allowed: use ${allowedMethods.join(', ')}` }));
557
+ }
558
+
529
559
  res.statusCode = 404;
530
560
  res.end(JSON.stringify({ error: 'Endpoint not found' }));
531
561
  },
package/lib/tools.js CHANGED
@@ -42,7 +42,7 @@ export function formatGoalValue(snap) {
42
42
  * @param {any} options.engine
43
43
  * @param {Function} [options.resumeActiveAgent]
44
44
  */
45
- export function registerTools(ctx, { engine, resumeActiveAgent }) {
45
+ export function registerTools(ctx, { engine, resumeActiveAgent, logger = console }) {
46
46
  ctx.inject(['tools'], (tctx) => {
47
47
  if (!tctx.tools?.register) return;
48
48
 
@@ -65,7 +65,7 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
65
65
  }, `dsh-goal: tool ${name}`);
66
66
  }
67
67
  } catch (err) {
68
- console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
68
+ logger.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
69
69
  }
70
70
  };
71
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
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",