@goodandready/dsh-goal 0.2.17 → 0.2.18

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.
@@ -10,9 +10,19 @@ export const DEFAULT_SETTINGS = {
10
10
  showQuickLaunchButton: true,
11
11
  consecutiveToolFailureLimit: 3,
12
12
  enableBrowserNotifications: true,
13
+ maxTokenBudget: 0,
14
+ budgetWarningThreshold: 80,
15
+ autoCheckpointOnMilestone: false,
13
16
  storagePath: '',
14
17
  };
15
18
 
19
+ export const NUMERIC_FIELDS = {
20
+ maxIterations: 1,
21
+ consecutiveToolFailureLimit: 0,
22
+ maxTokenBudget: 0,
23
+ budgetWarningThreshold: 0,
24
+ };
25
+
16
26
  /**
17
27
  * Parse numeric settings field:
18
28
  * - empty string / null / undefined -> undefined (reset override)
@@ -50,28 +60,14 @@ export function getFieldStatus(field, draftValue, snap) {
50
60
  let invalid = false;
51
61
  let isDirty = false;
52
62
 
53
- if (field === 'maxIterations') {
63
+ if (field in NUMERIC_FIELDS) {
54
64
  if (draftValue !== undefined) {
55
65
  if (draftValue === '') {
56
66
  invalid = false;
57
67
  isDirty = userVal !== undefined;
58
68
  } else {
59
- const parsed = parseNumberField(draftValue, 1);
60
- if (Number.isNaN(parsed)) {
61
- invalid = true;
62
- isDirty = true;
63
- } else {
64
- isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
65
- }
66
- }
67
- }
68
- } else if (field === 'consecutiveToolFailureLimit') {
69
- if (draftValue !== undefined) {
70
- if (draftValue === '') {
71
- invalid = false;
72
- isDirty = userVal !== undefined;
73
- } else {
74
- const parsed = parseNumberField(draftValue, 0);
69
+ const min = NUMERIC_FIELDS[field];
70
+ const parsed = parseNumberField(draftValue, min);
75
71
  if (Number.isNaN(parsed)) {
76
72
  invalid = true;
77
73
  isDirty = true;
@@ -86,7 +82,7 @@ export function getFieldStatus(field, draftValue, snap) {
86
82
  isDirty = cleanVal !== (userVal !== undefined ? userVal : (baseVal || ''));
87
83
  }
88
84
  } else {
89
- // boolean fields (autoDrive, enableSound, showQuickLaunchButton, enableBrowserNotifications)
85
+ // boolean fields (autoDrive, enableSound, showQuickLaunchButton, enableBrowserNotifications, autoCheckpointOnMilestone)
90
86
  if (draftValue !== undefined) {
91
87
  isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
92
88
  }
@@ -119,6 +115,9 @@ export function computeSavePlan(draft, snap) {
119
115
  'showQuickLaunchButton',
120
116
  'consecutiveToolFailureLimit',
121
117
  'enableBrowserNotifications',
118
+ 'maxTokenBudget',
119
+ 'budgetWarningThreshold',
120
+ 'autoCheckpointOnMilestone',
122
121
  'storagePath',
123
122
  ];
124
123
 
@@ -129,17 +128,11 @@ export function computeSavePlan(draft, snap) {
129
128
  if (status.invalid) continue;
130
129
 
131
130
  if (status.isDirty) {
132
- if (f === 'maxIterations') {
133
- if (draft[f] === '') {
134
- writes.push([f, undefined]);
135
- } else {
136
- writes.push([f, parseNumberField(draft[f], 1)]);
137
- }
138
- } else if (f === 'consecutiveToolFailureLimit') {
131
+ if (f in NUMERIC_FIELDS) {
139
132
  if (draft[f] === '') {
140
133
  writes.push([f, undefined]);
141
134
  } else {
142
- writes.push([f, parseNumberField(draft[f], 0)]);
135
+ writes.push([f, parseNumberField(draft[f], NUMERIC_FIELDS[f])]);
143
136
  }
144
137
  } else if (f === 'storagePath') {
145
138
  const clean = typeof draft[f] === 'string' ? draft[f].trim() : '';
package/lib/client.js CHANGED
@@ -3794,153 +3794,125 @@ window.__ModuleLoader__.load({
3794
3794
  // --- SETTINGS FORM LOGIC ---
3795
3795
 
3796
3796
  const DEFAULT_SETTINGS = {
3797
-
3798
3797
  maxIterations: 25,
3799
-
3800
3798
  autoDrive: true,
3801
-
3802
3799
  enableSound: true,
3803
-
3804
3800
  showQuickLaunchButton: true,
3805
-
3806
3801
  consecutiveToolFailureLimit: 3,
3807
-
3808
3802
  enableBrowserNotifications: true,
3809
-
3803
+ maxTokenBudget: 0,
3804
+ budgetWarningThreshold: 80,
3805
+ autoCheckpointOnMilestone: false,
3806
+ storagePath: '',
3810
3807
  };
3811
3808
 
3812
- function parseNumberField(raw) {
3809
+ const NUMERIC_FIELDS = {
3810
+ maxIterations: 1,
3811
+ consecutiveToolFailureLimit: 0,
3812
+ maxTokenBudget: 0,
3813
+ budgetWarningThreshold: 0,
3814
+ };
3813
3815
 
3816
+ function parseNumberField(raw, min = 1) {
3814
3817
  if (raw === '' || raw === null || raw === undefined) {
3815
-
3816
3818
  return undefined;
3817
-
3818
3819
  }
3819
-
3820
3820
  const n = Number(raw);
3821
-
3822
- if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
3823
-
3821
+ if (!Number.isFinite(n) || n < min || !Number.isInteger(n)) {
3824
3822
  return NaN;
3825
-
3826
3823
  }
3827
-
3828
3824
  return n;
3829
-
3830
3825
  }
3831
3826
 
3832
3827
  function getFieldStatus(field, draftValue, snap) {
3833
-
3834
3828
  const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
3835
-
3836
3829
  const userVal = snap?.user?.[field];
3837
-
3838
3830
  const effectiveVal = snap?.value?.[field] ?? baseVal;
3839
3831
 
3840
3832
  const isOverridden = userVal !== undefined && userVal !== null;
3841
-
3842
3833
  const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
3843
3834
 
3844
3835
  let invalid = false;
3845
-
3846
3836
  let isDirty = false;
3847
3837
 
3848
- if (field === 'maxIterations') {
3849
-
3838
+ if (field in NUMERIC_FIELDS) {
3850
3839
  if (draftValue !== undefined) {
3851
-
3852
3840
  if (draftValue === '') {
3853
-
3854
3841
  invalid = false;
3855
-
3856
3842
  isDirty = userVal !== undefined;
3857
-
3858
3843
  } else {
3859
-
3860
- const parsed = parseNumberField(draftValue);
3861
-
3844
+ const min = NUMERIC_FIELDS[field];
3845
+ const parsed = parseNumberField(draftValue, min);
3862
3846
  if (Number.isNaN(parsed)) {
3863
-
3864
3847
  invalid = true;
3865
-
3866
3848
  isDirty = true;
3867
-
3868
3849
  } else {
3869
-
3870
3850
  isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
3871
-
3872
3851
  }
3873
-
3874
3852
  }
3875
-
3876
3853
  }
3877
-
3854
+ } else if (field === 'storagePath') {
3855
+ if (draftValue !== undefined) {
3856
+ const cleanVal = typeof draftValue === 'string' ? draftValue.trim() : '';
3857
+ isDirty = cleanVal !== (userVal !== undefined ? userVal : (baseVal || ''));
3858
+ }
3878
3859
  } else {
3879
-
3880
3860
  if (draftValue !== undefined) {
3881
-
3882
3861
  isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
3883
-
3884
3862
  }
3885
-
3886
3863
  }
3887
3864
 
3888
3865
  return {
3889
-
3890
3866
  value: currentVal,
3891
-
3892
3867
  baseValue: baseVal,
3893
-
3894
3868
  userValue: userVal,
3895
-
3896
3869
  isOverridden,
3897
-
3898
3870
  isDirty,
3899
-
3900
3871
  invalid,
3901
-
3902
3872
  };
3903
-
3904
3873
  }
3905
3874
 
3906
3875
  function computeSavePlan(draft, snap) {
3907
-
3908
3876
  if (!draft) return [];
3909
3877
 
3910
- const fields = ['maxIterations', 'autoDrive', 'enableSound', 'showQuickLaunchButton'];
3878
+ const fields = [
3879
+ 'maxIterations',
3880
+ 'autoDrive',
3881
+ 'enableSound',
3882
+ 'showQuickLaunchButton',
3883
+ 'consecutiveToolFailureLimit',
3884
+ 'enableBrowserNotifications',
3885
+ 'maxTokenBudget',
3886
+ 'budgetWarningThreshold',
3887
+ 'autoCheckpointOnMilestone',
3888
+ 'storagePath',
3889
+ ];
3911
3890
 
3912
3891
  const writes = [];
3913
3892
 
3914
3893
  for (const f of fields) {
3915
-
3916
3894
  if (draft[f] === undefined) continue;
3917
3895
 
3918
3896
  const status = getFieldStatus(f, draft[f], snap);
3919
-
3920
3897
  if (status.invalid) continue;
3921
3898
 
3922
3899
  if (status.isDirty) {
3923
-
3924
- if (f === 'maxIterations' && draft[f] === '') {
3925
-
3926
- writes.push([f, undefined]);
3927
-
3928
- } else if (f === 'maxIterations') {
3929
-
3930
- writes.push([f, parseNumberField(draft[f])]);
3931
-
3900
+ if (f in NUMERIC_FIELDS) {
3901
+ if (draft[f] === '') {
3902
+ writes.push([f, undefined]);
3903
+ } else {
3904
+ writes.push([f, parseNumberField(draft[f], NUMERIC_FIELDS[f])]);
3905
+ }
3906
+ } else if (f === 'storagePath') {
3907
+ const clean = typeof draft[f] === 'string' ? draft[f].trim() : '';
3908
+ writes.push([f, clean ? clean : undefined]);
3932
3909
  } else {
3933
-
3934
3910
  writes.push([f, Boolean(draft[f])]);
3935
-
3936
3911
  }
3937
-
3938
3912
  }
3939
-
3940
3913
  }
3941
3914
 
3942
3915
  return writes;
3943
-
3944
3916
  }
3945
3917
 
3946
3918
  // --- SETTINGS CARD ---
@@ -4138,17 +4110,11 @@ window.__ModuleLoader__.load({
4138
4110
  };
4139
4111
 
4140
4112
  const resetFieldToDefault = (field) => {
4141
-
4142
- if (field === 'maxIterations') {
4143
-
4113
+ if (field in NUMERIC_FIELDS) {
4144
4114
  edit(field, '');
4145
-
4146
4115
  } else {
4147
-
4148
4116
  edit(field, snap?.base?.[field] ?? DEFAULT_SETTINGS[field]);
4149
-
4150
4117
  }
4151
-
4152
4118
  };
4153
4119
 
4154
4120
  const save = async () => {
@@ -4612,7 +4578,7 @@ window.__ModuleLoader__.load({
4612
4578
 
4613
4579
  title: t('resetField') || 'Reset to default',
4614
4580
 
4615
- onClick: () => edit('consecutiveToolFailureLimit', snap?.base?.consecutiveToolFailureLimit ?? DEFAULT_SETTINGS.consecutiveToolFailureLimit),
4581
+ onClick: () => resetFieldToDefault('consecutiveToolFailureLimit'),
4616
4582
 
4617
4583
  },
4618
4584
 
@@ -4644,7 +4610,7 @@ window.__ModuleLoader__.load({
4644
4610
 
4645
4611
  className: 'dsh-goal-card-input',
4646
4612
 
4647
- onChange: (e) => edit('consecutiveToolFailureLimit', parseNumberField(e.target.value)),
4613
+ onChange: (e) => edit('consecutiveToolFailureLimit', e.target.value),
4648
4614
 
4649
4615
  }),
4650
4616
 
@@ -4680,7 +4646,7 @@ window.__ModuleLoader__.load({
4680
4646
  type: 'button',
4681
4647
  className: 'dsh-goal-btn-inline-reset',
4682
4648
  title: t('resetField') || 'Reset to default',
4683
- onClick: () => edit('maxTokenBudget', snap?.base?.maxTokenBudget ?? DEFAULT_SETTINGS.maxTokenBudget),
4649
+ onClick: () => resetFieldToDefault('maxTokenBudget'),
4684
4650
  },
4685
4651
  React.createElement(IconRotateCcw, { size: 12 }),
4686
4652
  t('resetField') || 'Default'
@@ -4697,7 +4663,7 @@ window.__ModuleLoader__.load({
4697
4663
  value: budgetStatus.value !== undefined ? String(budgetStatus.value) : '',
4698
4664
  disabled,
4699
4665
  className: 'dsh-goal-card-input',
4700
- onChange: (e) => edit('maxTokenBudget', parseNumberField(e.target.value)),
4666
+ onChange: (e) => edit('maxTokenBudget', e.target.value),
4701
4667
  })
4702
4668
  ),
4703
4669
 
@@ -4721,7 +4687,7 @@ window.__ModuleLoader__.load({
4721
4687
  type: 'button',
4722
4688
  className: 'dsh-goal-btn-inline-reset',
4723
4689
  title: t('resetField') || 'Reset to default',
4724
- onClick: () => edit('budgetWarningThreshold', snap?.base?.budgetWarningThreshold ?? DEFAULT_SETTINGS.budgetWarningThreshold),
4690
+ onClick: () => resetFieldToDefault('budgetWarningThreshold'),
4725
4691
  },
4726
4692
  React.createElement(IconRotateCcw, { size: 12 }),
4727
4693
  t('resetField') || 'Default'
@@ -4738,7 +4704,7 @@ window.__ModuleLoader__.load({
4738
4704
  value: budgetWarnStatus.value !== undefined ? String(budgetWarnStatus.value) : '',
4739
4705
  disabled,
4740
4706
  className: 'dsh-goal-card-input',
4741
- onChange: (e) => edit('budgetWarningThreshold', parseNumberField(e.target.value)),
4707
+ onChange: (e) => edit('budgetWarningThreshold', e.target.value),
4742
4708
  })
4743
4709
  ),
4744
4710
 
package/lib/index.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import z from '@deepseek-ai/schemastery';
4
+ // Polyfill volatile schema modifier if running against schemastery < 3.18.4
5
+ if (typeof z.Schema?.prototype?.volatile !== 'function') {
6
+ const proto = z.Schema ? z.Schema.prototype : (typeof z === 'function' ? Object.getPrototypeOf(z.string?.() || {}) : null);
7
+ if (proto && typeof proto.extra === 'function' && typeof proto.volatile !== 'function') {
8
+ proto.volatile = function volatile() {
9
+ if (this.meta?.volatile) throw new TypeError('volatile schema is already wrapped');
10
+ return this.extra('volatile', true);
11
+ };
12
+ }
13
+ }
14
+
4
15
  import { AddonStore } from './addon-store.js';
5
16
  import { TokenBudgetGuard } from './token-budget-guard.js';
6
17
  import { GitSnapshotManager } from './git-snapshot-manager.js';
@@ -70,27 +81,28 @@ export function safeService(ctx, name) {
70
81
 
71
82
  export function apply(ctx, config = {}) {
72
83
  const sseClients = new Map();
84
+ const cfg = plainConfig(config || {});
73
85
 
74
86
  let currentSettings = {
75
- maxIterations: config?.maxIterations ?? 25,
76
- autoDrive: config?.autoDrive ?? true,
77
- enableSound: config?.enableSound ?? true,
78
- showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
79
- consecutiveToolFailureLimit: config?.consecutiveToolFailureLimit ?? 3,
80
- enableBrowserNotifications: config?.enableBrowserNotifications ?? true,
81
- maxTokenBudget: config?.maxTokenBudget ?? 0,
82
- budgetWarningThreshold: config?.budgetWarningThreshold ?? 80,
83
- autoCheckpointOnMilestone: config?.autoCheckpointOnMilestone ?? false,
84
- enableLiveActivityFeed: config?.enableLiveActivityFeed ?? true,
85
- enablePreplanning: config?.enablePreplanning ?? true,
86
- autoBranchOnGoalStart: config?.autoBranchOnGoalStart ?? true,
87
- enablePostGoalRetrospective: config?.enablePostGoalRetrospective ?? true,
88
- autoScaleBudgetNearCompletion: config?.autoScaleBudgetNearCompletion ?? false,
89
- enableTemplatesDrawer: config?.enableTemplatesDrawer ?? true,
90
- enableMilestoneDependencies: config?.enableMilestoneDependencies ?? true,
91
- soundScheme: config?.soundScheme ?? 'default',
92
- enableVoiceAnnouncements: config?.enableVoiceAnnouncements ?? false,
93
- storagePath: config?.storagePath,
87
+ maxIterations: cfg?.maxIterations ?? 25,
88
+ autoDrive: cfg?.autoDrive ?? true,
89
+ enableSound: cfg?.enableSound ?? true,
90
+ showQuickLaunchButton: cfg?.showQuickLaunchButton ?? true,
91
+ consecutiveToolFailureLimit: cfg?.consecutiveToolFailureLimit ?? 3,
92
+ enableBrowserNotifications: cfg?.enableBrowserNotifications ?? true,
93
+ maxTokenBudget: cfg?.maxTokenBudget ?? 0,
94
+ budgetWarningThreshold: cfg?.budgetWarningThreshold ?? 80,
95
+ autoCheckpointOnMilestone: cfg?.autoCheckpointOnMilestone ?? false,
96
+ enableLiveActivityFeed: cfg?.enableLiveActivityFeed ?? true,
97
+ enablePreplanning: cfg?.enablePreplanning ?? true,
98
+ autoBranchOnGoalStart: cfg?.autoBranchOnGoalStart ?? true,
99
+ enablePostGoalRetrospective: cfg?.enablePostGoalRetrospective ?? true,
100
+ autoScaleBudgetNearCompletion: cfg?.autoScaleBudgetNearCompletion ?? false,
101
+ enableTemplatesDrawer: cfg?.enableTemplatesDrawer ?? true,
102
+ enableMilestoneDependencies: cfg?.enableMilestoneDependencies ?? true,
103
+ soundScheme: cfg?.soundScheme ?? 'default',
104
+ enableVoiceAnnouncements: cfg?.enableVoiceAnnouncements ?? false,
105
+ storagePath: cfg?.storagePath,
94
106
  };
95
107
 
96
108
  const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
@@ -298,6 +310,23 @@ export function apply(ctx, config = {}) {
298
310
  if (typeof patch.storagePath === 'string' && patch.storagePath.trim()) {
299
311
  addonStore.setStoragePath(patch.storagePath.trim());
300
312
  }
313
+ if (fallbackEngine && typeof fallbackEngine.updateConfig === 'function') {
314
+ fallbackEngine.updateConfig({
315
+ defaultMaxIterations: currentSettings.maxIterations,
316
+ autoDrive: currentSettings.autoDrive,
317
+ consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
318
+ maxTokenBudget: currentSettings.maxTokenBudget,
319
+ budgetWarningThreshold: currentSettings.budgetWarningThreshold,
320
+ autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
321
+ enableLiveActivityFeed: currentSettings.enableLiveActivityFeed,
322
+ enablePreplanning: currentSettings.enablePreplanning,
323
+ autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
324
+ enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
325
+ autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
326
+ enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
327
+ enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
328
+ });
329
+ }
301
330
  };
302
331
 
303
332
  // DSH hands the plugin the entry config, whose .volatile() fields hold Volatile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
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",