@goodandready/dsh-goal 0.2.4 → 0.2.6

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/README.md CHANGED
@@ -202,6 +202,9 @@ dsh-goal:
202
202
  showQuickLaunchButton: true
203
203
  consecutiveToolFailureLimit: 3
204
204
  enableBrowserNotifications: true
205
+ maxTokenBudget: 0
206
+ budgetWarningThreshold: 80
207
+ autoCheckpointOnMilestone: false
205
208
  ```
206
209
 
207
210
  | Parameter | Type | Default | Description |
@@ -212,6 +215,9 @@ dsh-goal:
212
215
  | `showQuickLaunchButton` | `boolean` | `true` | Show the quick launch goal button above the composer dock |
213
216
  | `consecutiveToolFailureLimit` | `number` | `3` | Auto-pause goal if N consecutive turns hit tool execution errors (0 to disable) |
214
217
  | `enableBrowserNotifications` | `boolean` | `true` | Show native desktop push notifications on goal completion or failure |
218
+ | `maxTokenBudget` | `number` | `0` | Safety token ceiling (0 = disabled); triggers soft-pause when exceeded |
219
+ | `budgetWarningThreshold` | `number` | `80` | Percentage of budget consumed before warning model to wrap up |
220
+ | `autoCheckpointOnMilestone` | `boolean` | `false` | Automatically create git snapshot commits upon milestone completion |
215
221
 
216
222
  ---
217
223
 
@@ -229,6 +235,13 @@ npm test
229
235
 
230
236
  ## 📦 Release Notes
231
237
 
238
+ ### v0.2.6
239
+ - **Token Budget Guard**: Safety ceiling with proactive model guidance at threshold (default 80%) and soft pause (`PAUSED_BUDGET_EXCEEDED`) with 1-click +50k extension.
240
+ - **Sub-milestones & Checklists**: Granular sub-tasks per milestone (`checklist: [{ text, done }]`) with real-time UI toggling and model prompt synchronization.
241
+ - **Auto Git Checkpoints**: Optional automatic git snapshotting upon milestone completion with one-click rollback in the details modal.
242
+ - **Run Artifact Export**: One-click export of structured execution runs to `.dsh/goals/<timestamp>-<slug>.md` (with `.dsh/` git-ignored).
243
+ - **Compact Dock Mode**: Minimizable dock banner switching to sleek status pill, state persisted in `localStorage`.
244
+
232
245
  ### v0.2.3
233
246
  - **Auto-Updater**: Integrated one-click update API endpoint at `/api/dsh-goal/update` with fail-closed origin checks.
234
247
  - **Modular Architecture**: Decomposed server codebase into focused submodules (<600 lines each) for routes, tools, store, reports, and updater.
package/README.ru.md CHANGED
@@ -159,6 +159,13 @@ npm test
159
159
 
160
160
  ## 📦 История версий (Release Notes)
161
161
 
162
+ ### v0.2.6
163
+ - **Token Budget Guard**: Защитный потолок расхода токенов с предупреждением модели на 80% и мягкой паузой (`PAUSED_BUDGET_EXCEEDED`) с продлением +50k в 1 клик.
164
+ - **Подзадачи и чек-листы (Sub-milestones)**: Интерактивные чекбоксы подзадач внутри каждого этапа (`checklist: [{ text, done }]`) с синхронизацией с моделью.
165
+ - **Автоматические Git-чекпоинты**: Опциональное создание снимков репозитория при завершении этапа с откатом в 1 клик.
166
+ - **Экспорт артефактов выполнения**: Сохранение полного отчёта в `.dsh/goals/<timestamp>-<slug>.md` (каталог `.dsh/` добавлен в `.gitignore`).
167
+ - **Компактный режим панели (Compact Dock Mode)**: Сворачивание панели цели в аккуратную капсулу с запоминанием в `localStorage`.
168
+
162
169
  ### v0.2.3
163
170
  - **Автообновление**: Встроен эндпоинт API автообновления в один клик по адресу `/api/dsh-goal/update` с fail-closed проверкой источника.
164
171
  - **Модульная архитектура**: Серверная половина плагина полностью декомпозирована на специализированные модули (<600 строк каждый): маршруты, инструменты, хранилище, отчёты и модуль обновления.
package/README.zh.md CHANGED
@@ -224,6 +224,13 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
224
224
 
225
225
  ## 📦 版本历史 (Release Notes)
226
226
 
227
+ ### v0.2.6
228
+ - **Token 预算守护 (Token Budget Guard)**:设置安全使用上限,并在消耗达阈值(默认80%)时注入预警引导,达到100%时软暂停(`PAUSED_BUDGET_EXCEEDED`),支持一键延展 +50k 预算。
229
+ - **子里程碑与检查清单 (Sub-milestones & Checklists)**:支持里程碑层级下的细分任务清单(`checklist: [{ text, done }]`),支持前端实时勾选及提示词自动同步。
230
+ - **Git 自动检查点 (Auto Git Checkpoints)**:里程碑达成时可选自动生成快照提交,并在详情弹窗中支持一键回滚。
231
+ - **执行成果导出 (Run Artifact Export)**:一键导出 Markdown 格式执行成果至 `.dsh/goals/<timestamp>-<slug>.md`(已添加 `.dsh/` 至 `.gitignore`)。
232
+ - **紧凑面板模式 (Compact Dock Mode)**:输入栏固定面板可折叠为精巧胶囊状态栏,状态保存至 `localStorage`。
233
+
227
234
  ### v0.2.3
228
235
  - **自动更新**: 在 `/api/dsh-goal/update` 集成了一键更新 API 端点,并配备安全的原点验证。
229
236
  - **模块化架构**: 将服务端完整解耦为高内聚子模块(各模块均低于 600 行):路由、工具、持久化存储、报告生成与更新器。
package/lib/client.js CHANGED
@@ -282,6 +282,46 @@ window.__ModuleLoader__.load({
282
282
 
283
283
  }
284
284
 
285
+ function IconMinimize({ size = 14, className = '' }) {
286
+
287
+ return React.createElement(
288
+
289
+ 'svg',
290
+
291
+ {
292
+
293
+ width: size,
294
+
295
+ height: size,
296
+
297
+ viewBox: '0 0 24 24',
298
+
299
+ fill: 'none',
300
+
301
+ stroke: 'currentColor',
302
+
303
+ strokeWidth: 1.8,
304
+
305
+ strokeLinecap: 'round',
306
+
307
+ strokeLinejoin: 'round',
308
+
309
+ className,
310
+
311
+ },
312
+
313
+ React.createElement('polyline', { points: '4 14 10 14 10 20' }),
314
+
315
+ React.createElement('polyline', { points: '20 10 14 10 14 4' }),
316
+
317
+ React.createElement('line', { x1: 14, y1: 10, x2: 21, y2: 3 }),
318
+
319
+ React.createElement('line', { x1: 3, y1: 21, x2: 10, y2: 14 }),
320
+
321
+ );
322
+
323
+ }
324
+
285
325
  function IconRotateCcw({ size = 13, className = '' }) {
286
326
 
287
327
  return React.createElement(
@@ -372,6 +412,54 @@ window.__ModuleLoader__.load({
372
412
 
373
413
  }
374
414
 
415
+ .dsh-goal-dock-pill {
416
+ display: inline-flex;
417
+ align-items: center;
418
+ gap: 8px;
419
+ background: var(--dsw-alias-surface-raised);
420
+ border: 1px solid var(--dsw-alias-border-subtle);
421
+ border-radius: 9999px;
422
+ padding: 4px 12px;
423
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
424
+ backdrop-filter: blur(8px);
425
+ font-size: 13px;
426
+ max-width: 100%;
427
+ box-sizing: border-box;
428
+ }
429
+ .dsh-goal-dock-pill .dsh-goal-title {
430
+ max-width: 260px;
431
+ white-space: nowrap;
432
+ overflow: hidden;
433
+ text-overflow: ellipsis;
434
+ font-weight: 500;
435
+ }
436
+ .dsh-goal-checklist {
437
+ margin-top: 6px;
438
+ display: flex;
439
+ flex-direction: column;
440
+ gap: 4px;
441
+ }
442
+ .dsh-goal-checklist-item {
443
+ display: flex;
444
+ align-items: center;
445
+ gap: 6px;
446
+ font-size: 12px;
447
+ color: var(--dsw-alias-label-secondary);
448
+ cursor: pointer;
449
+ user-select: none;
450
+ }
451
+ .dsh-goal-checklist-item input[type="checkbox"] {
452
+ cursor: pointer;
453
+ margin: 0;
454
+ }
455
+ .dsh-goal-budget-warn {
456
+ color: #f59e0b;
457
+ font-weight: 600;
458
+ display: inline-flex;
459
+ align-items: center;
460
+ gap: 4px;
461
+ font-size: 12px;
462
+ }
375
463
  .dsh-goal-dock.dsh-goal-dock-quick {
376
464
 
377
465
  justify-content: flex-start;
@@ -1646,6 +1734,10 @@ window.__ModuleLoader__.load({
1646
1734
 
1647
1735
  const [gitCopied, setGitCopied] = useState(false);
1648
1736
 
1737
+ const [artifactSaved, setArtifactSaved] = useState(false);
1738
+
1739
+ const [savingArtifact, setSavingArtifact] = useState(false);
1740
+
1649
1741
  const [nudgeText, setNudgeText] = useState('');
1650
1742
 
1651
1743
  const [nudgeSent, setNudgeSent] = useState(false);
@@ -2010,6 +2102,51 @@ window.__ModuleLoader__.load({
2010
2102
 
2011
2103
  m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
2012
2104
 
2105
+ Array.isArray(m.checklist) && m.checklist.length > 0
2106
+ ? React.createElement(
2107
+ 'div',
2108
+ { className: 'dsh-goal-checklist' },
2109
+ m.checklist.map((item, itemIdx) =>
2110
+ React.createElement(
2111
+ 'label',
2112
+ { key: itemIdx, className: 'dsh-goal-checklist-item' },
2113
+ React.createElement('input', {
2114
+ type: 'checkbox',
2115
+ checked: Boolean(item.done),
2116
+ onChange: () => onAction?.('toggle_checklist_item', { milestoneId: m.id, itemIndex: itemIdx, done: !item.done }),
2117
+ }),
2118
+ React.createElement(
2119
+ 'span',
2120
+ { style: { textDecoration: item.done ? 'line-through' : 'none', color: item.done ? 'var(--dsw-alias-label-tertiary)' : 'inherit' } },
2121
+ item.text
2122
+ )
2123
+ )
2124
+ )
2125
+ )
2126
+ : null,
2127
+
2128
+ m.checkpointCommit
2129
+ ? React.createElement(
2130
+ 'div',
2131
+ { style: { marginTop: 4 } },
2132
+ React.createElement(
2133
+ 'button',
2134
+ {
2135
+ type: 'button',
2136
+ className: 'dsh-goal-btn-inline-reset',
2137
+ style: { fontSize: 11, padding: '1px 6px' },
2138
+ title: t('rollbackBtn') || 'Rollback to Checkpoint',
2139
+ onClick: () => {
2140
+ if (confirm(t('rollbackConfirm') || `Rollback working tree to checkpoint ${m.checkpointCommit}?`)) {
2141
+ onAction?.('rollback_milestone', { commit: m.checkpointCommit });
2142
+ }
2143
+ },
2144
+ },
2145
+ `↩ ${m.checkpointCommit}`
2146
+ )
2147
+ )
2148
+ : null,
2149
+
2013
2150
  ),
2014
2151
 
2015
2152
  ),
@@ -2086,6 +2223,28 @@ window.__ModuleLoader__.load({
2086
2223
 
2087
2224
  ),
2088
2225
 
2226
+ React.createElement(
2227
+ 'button',
2228
+ {
2229
+ className: `dsh-goal-btn dsh-goal-btn-copy${artifactSaved ? ' copied' : ''}`,
2230
+ disabled: savingArtifact,
2231
+ onClick: async () => {
2232
+ setSavingArtifact(true);
2233
+ try {
2234
+ await onAction?.('save_artifact');
2235
+ setArtifactSaved(true);
2236
+ setTimeout(() => setArtifactSaved(false), 3000);
2237
+ } catch (err) {
2238
+ console.warn('[dsh-goal] Save artifact error:', err);
2239
+ } finally {
2240
+ setSavingArtifact(false);
2241
+ }
2242
+ },
2243
+ title: t('saveArtifact') || 'Save Run Artifact to .dsh/goals/',
2244
+ },
2245
+ artifactSaved ? (t('artifactSaved') || '✅ Saved to .dsh/goals/') : (t('saveArtifact') || '💾 Save Run Artifact')
2246
+ ),
2247
+
2089
2248
  )
2090
2249
 
2091
2250
  : null,
@@ -2408,6 +2567,24 @@ window.__ModuleLoader__.load({
2408
2567
 
2409
2568
  const [isQuickLaunchOpen, setIsQuickLaunchOpen] = useState(false);
2410
2569
 
2570
+ const [isDockCollapsed, setIsDockCollapsed] = useState(() => {
2571
+ try {
2572
+ return localStorage.getItem('dsh_goal_dock_collapsed') === 'true';
2573
+ } catch (e) {
2574
+ return false;
2575
+ }
2576
+ });
2577
+
2578
+ const toggleDockCollapse = () => {
2579
+ setIsDockCollapsed((prev) => {
2580
+ const next = !prev;
2581
+ try {
2582
+ localStorage.setItem('dsh_goal_dock_collapsed', String(next));
2583
+ } catch (e) {}
2584
+ return next;
2585
+ });
2586
+ };
2587
+
2411
2588
  const [now, setNow] = useState(() => Date.now());
2412
2589
 
2413
2590
  const stateRef = useRef(null);
@@ -2926,6 +3103,8 @@ window.__ModuleLoader__.load({
2926
3103
 
2927
3104
  const etaStr = state?.formattedETA ? ` (ETA ${state.formattedETA})` : '';
2928
3105
 
3106
+ const isBudgetExceeded = state?.state === 'PAUSED' && state?.logs?.some((l) => l.message?.includes('Token budget reached'));
3107
+
2929
3108
  return React.createElement(
2930
3109
 
2931
3110
  React.Fragment,
@@ -2938,7 +3117,54 @@ window.__ModuleLoader__.load({
2938
3117
 
2939
3118
  { className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
2940
3119
 
2941
- React.createElement(
3120
+ isDockCollapsed
3121
+ ? React.createElement(
3122
+ 'div',
3123
+ { className: 'dsh-goal-dock-pill' },
3124
+ React.createElement(
3125
+ 'span',
3126
+ { className: 'dsh-goal-icon' },
3127
+ isCompleted ? React.createElement(IconCheck, { size: 14 }) : React.createElement(IconTarget, { size: 13 })
3128
+ ),
3129
+ React.createElement(
3130
+ 'span',
3131
+ { className: `dsh-goal-badge ${isCompleted ? 'dsh-goal-badge-ok' : isPaused ? 'dsh-goal-badge-warn' : ''}` },
3132
+ isCompleted ? (t('goalCompleted') || 'Done') : isPaused ? (t('paused') || 'Paused') : (t('goalLabel') || 'Goal')
3133
+ ),
3134
+ React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
3135
+ React.createElement('span', { className: 'dsh-goal-time' }, `⏱ ${liveElapsedStr}`),
3136
+ !isCompleted ? React.createElement(
3137
+ 'button',
3138
+ {
3139
+ className: 'dsh-goal-btn icon-only',
3140
+ style: { padding: '2px 4px', height: 22 },
3141
+ title: isPaused ? (t('resume') || 'Resume') : (t('pause') || 'Pause'),
3142
+ onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
3143
+ },
3144
+ isPaused ? React.createElement(IconPlay, { size: 12 }) : React.createElement(IconPause, { size: 12 })
3145
+ ) : null,
3146
+ React.createElement(
3147
+ 'button',
3148
+ {
3149
+ className: 'dsh-goal-btn icon-only',
3150
+ style: { padding: '2px 4px', height: 22 },
3151
+ title: t('details') || 'Expand goal details',
3152
+ onClick: () => setIsModalOpen(true),
3153
+ },
3154
+ React.createElement(IconExpand, { size: 12 })
3155
+ ),
3156
+ React.createElement(
3157
+ 'button',
3158
+ {
3159
+ className: 'dsh-goal-btn icon-only',
3160
+ style: { padding: '2px 4px', height: 22 },
3161
+ title: t('expandDock') || 'Expand goal banner',
3162
+ onClick: toggleDockCollapse,
3163
+ },
3164
+ '⤢'
3165
+ )
3166
+ )
3167
+ : React.createElement(
2942
3168
 
2943
3169
  'div',
2944
3170
 
@@ -3016,6 +3242,37 @@ window.__ModuleLoader__.load({
3016
3242
 
3017
3243
  : null,
3018
3244
 
3245
+ isBudgetExceeded
3246
+ ? React.createElement(
3247
+ 'button',
3248
+ {
3249
+ className: 'dsh-goal-btn',
3250
+ style: { background: '#f59e0b', color: '#000', fontWeight: 600, fontSize: 12, padding: '2px 8px' },
3251
+ title: t('extendBudgetBtn') || '+50k Tokens & Resume',
3252
+ onClick: () => handleAction('extend_budget', { addTokens: 50000 }),
3253
+ },
3254
+ t('extendBudgetBtn') || '+50k & Resume'
3255
+ )
3256
+ : null,
3257
+
3258
+ React.createElement(
3259
+
3260
+ 'button',
3261
+
3262
+ {
3263
+
3264
+ className: 'dsh-goal-btn icon-only',
3265
+
3266
+ title: t('compactDock') || 'Collapse to compact pill',
3267
+
3268
+ onClick: toggleDockCollapse,
3269
+
3270
+ },
3271
+
3272
+ React.createElement(IconMinimize, { size: 14 }),
3273
+
3274
+ ),
3275
+
3019
3276
  React.createElement(
3020
3277
 
3021
3278
  'button',
@@ -3386,9 +3643,15 @@ window.__ModuleLoader__.load({
3386
3643
 
3387
3644
  const notifStatus = getFieldStatus('enableBrowserNotifications', draft?.enableBrowserNotifications, snap);
3388
3645
 
3389
- const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty || quickLaunchStatus.isDirty || toolFailStatus.isDirty || notifStatus.isDirty;
3646
+ const budgetStatus = getFieldStatus('maxTokenBudget', draft?.maxTokenBudget, snap);
3647
+
3648
+ const budgetWarnStatus = getFieldStatus('budgetWarningThreshold', draft?.budgetWarningThreshold, snap);
3649
+
3650
+ const autoCheckpointStatus = getFieldStatus('autoCheckpointOnMilestone', draft?.autoCheckpointOnMilestone, snap);
3651
+
3652
+ const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty || quickLaunchStatus.isDirty || toolFailStatus.isDirty || notifStatus.isDirty || budgetStatus.isDirty || budgetWarnStatus.isDirty || autoCheckpointStatus.isDirty;
3390
3653
 
3391
- const invalid = maxIterStatus.invalid || toolFailStatus.invalid;
3654
+ const invalid = maxIterStatus.invalid || toolFailStatus.invalid || budgetStatus.invalid || budgetWarnStatus.invalid;
3392
3655
 
3393
3656
  const disabled = !scope || saving || snap?.writable === false;
3394
3657
 
@@ -3410,6 +3673,12 @@ window.__ModuleLoader__.load({
3410
3673
 
3411
3674
  enableBrowserNotifications: prev?.enableBrowserNotifications !== undefined ? prev.enableBrowserNotifications : notifStatus.value,
3412
3675
 
3676
+ maxTokenBudget: prev?.maxTokenBudget !== undefined ? prev.maxTokenBudget : budgetStatus.value,
3677
+
3678
+ budgetWarningThreshold: prev?.budgetWarningThreshold !== undefined ? prev.budgetWarningThreshold : budgetWarnStatus.value,
3679
+
3680
+ autoCheckpointOnMilestone: prev?.autoCheckpointOnMilestone !== undefined ? prev.autoCheckpointOnMilestone : autoCheckpointStatus.value,
3681
+
3413
3682
  [field]: val,
3414
3683
 
3415
3684
  }));
@@ -3939,6 +4208,101 @@ window.__ModuleLoader__.load({
3939
4208
 
3940
4209
  ),
3941
4210
 
4211
+ React.createElement(
4212
+ 'div',
4213
+ { className: 'dsh-goal-card-field' },
4214
+ React.createElement(
4215
+ 'div',
4216
+ { className: 'dsh-goal-field-label-row' },
4217
+ React.createElement('label', { htmlFor: 'dsh-goal-token-budget' }, t('budgetLabel') || 'Token Budget (0 for unlimited):'),
4218
+ React.createElement(
4219
+ 'div',
4220
+ { className: 'dsh-goal-field-meta' },
4221
+ budgetStatus.isOverridden
4222
+ ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'overridden')
4223
+ : null,
4224
+ budgetStatus.isOverridden
4225
+ ? React.createElement(
4226
+ 'button',
4227
+ {
4228
+ type: 'button',
4229
+ className: 'dsh-goal-btn-inline-reset',
4230
+ title: t('resetField') || 'Reset to default',
4231
+ onClick: () => edit('maxTokenBudget', snap?.base?.maxTokenBudget ?? DEFAULT_SETTINGS.maxTokenBudget),
4232
+ },
4233
+ React.createElement(IconRotateCcw, { size: 12 }),
4234
+ t('resetField') || 'Default'
4235
+ )
4236
+ : null
4237
+ )
4238
+ ),
4239
+ React.createElement('input', {
4240
+ id: 'dsh-goal-token-budget',
4241
+ type: 'number',
4242
+ min: 0,
4243
+ step: 10000,
4244
+ placeholder: String(budgetStatus.baseValue),
4245
+ value: budgetStatus.value !== undefined ? String(budgetStatus.value) : '',
4246
+ disabled,
4247
+ className: 'dsh-goal-card-input',
4248
+ onChange: (e) => edit('maxTokenBudget', parseNumberField(e.target.value)),
4249
+ })
4250
+ ),
4251
+
4252
+ React.createElement(
4253
+ 'div',
4254
+ { className: 'dsh-goal-card-field' },
4255
+ React.createElement(
4256
+ 'div',
4257
+ { className: 'dsh-goal-field-label-row' },
4258
+ React.createElement('label', { htmlFor: 'dsh-goal-warn-thresh' }, t('budgetWarnLabel') || 'Budget Warning Threshold (%):'),
4259
+ React.createElement(
4260
+ 'div',
4261
+ { className: 'dsh-goal-field-meta' },
4262
+ budgetWarnStatus.isOverridden
4263
+ ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'overridden')
4264
+ : null,
4265
+ budgetWarnStatus.isOverridden
4266
+ ? React.createElement(
4267
+ 'button',
4268
+ {
4269
+ type: 'button',
4270
+ className: 'dsh-goal-btn-inline-reset',
4271
+ title: t('resetField') || 'Reset to default',
4272
+ onClick: () => edit('budgetWarningThreshold', snap?.base?.budgetWarningThreshold ?? DEFAULT_SETTINGS.budgetWarningThreshold),
4273
+ },
4274
+ React.createElement(IconRotateCcw, { size: 12 }),
4275
+ t('resetField') || 'Default'
4276
+ )
4277
+ : null
4278
+ )
4279
+ ),
4280
+ React.createElement('input', {
4281
+ id: 'dsh-goal-warn-thresh',
4282
+ type: 'number',
4283
+ min: 10,
4284
+ max: 100,
4285
+ placeholder: String(budgetWarnStatus.baseValue),
4286
+ value: budgetWarnStatus.value !== undefined ? String(budgetWarnStatus.value) : '',
4287
+ disabled,
4288
+ className: 'dsh-goal-card-input',
4289
+ onChange: (e) => edit('budgetWarningThreshold', parseNumberField(e.target.value)),
4290
+ })
4291
+ ),
4292
+
4293
+ React.createElement(
4294
+ 'div',
4295
+ { className: 'dsh-goal-check-row' },
4296
+ React.createElement('input', {
4297
+ id: 'dsh-goal-auto-checkpoint',
4298
+ type: 'checkbox',
4299
+ checked: Boolean(autoCheckpointStatus.value),
4300
+ disabled,
4301
+ onChange: (e) => edit('autoCheckpointOnMilestone', e.target.checked),
4302
+ }),
4303
+ React.createElement('label', { htmlFor: 'dsh-goal-auto-checkpoint' }, t('autoCheckpointLabel') || 'Auto-create Git checkpoint on milestone completed')
4304
+ ),
4305
+
3942
4306
  React.createElement(
3943
4307
 
3944
4308
  'div',
@@ -4077,6 +4441,19 @@ window.__ModuleLoader__.load({
4077
4441
  checking: 'Checking…',
4078
4442
  updateDone: 'Updated successfully. Please restart DSH to apply.',
4079
4443
  updateFailed: 'Update failed: ',
4444
+ compactDock: 'Collapse to compact pill',
4445
+ expandDock: 'Expand goal banner',
4446
+ saveArtifact: 'Save Run Artifact',
4447
+ artifactSaved: '✅ Artifact Saved (.dsh/goals/)',
4448
+ saveArtifactFailed: 'Artifact save failed',
4449
+ extendBudgetBtn: '+50k Tokens & Resume',
4450
+ budgetExceededBadge: 'Budget Limit Reached',
4451
+ budgetLabel: 'Token Budget (0 for unlimited):',
4452
+ budgetWarnLabel: 'Budget Warning Threshold (%):',
4453
+ autoCheckpointLabel: 'Auto-create Git checkpoint on milestone completed',
4454
+ rollbackBtn: 'Rollback to Checkpoint',
4455
+ rollbackConfirm: 'Rollback working tree to this checkpoint?',
4456
+ rollbackSuccess: 'Rolled back to checkpoint',
4080
4457
 
4081
4458
  goalCompleted: 'Goal completed',
4082
4459
 
@@ -4190,6 +4567,19 @@ window.__ModuleLoader__.load({
4190
4567
  checking: '正在检查…',
4191
4568
  updateDone: '更新成功。请重启 DSH 以生效。',
4192
4569
  updateFailed: '更新失败:',
4570
+ compactDock: '收起为迷你胶囊',
4571
+ expandDock: '展开目标横幅',
4572
+ saveArtifact: '导出运行报告工件',
4573
+ artifactSaved: '✅ 工件已保存 (.dsh/goals/)',
4574
+ saveArtifactFailed: '工件保存失败',
4575
+ extendBudgetBtn: '追加 50k Tokens 并恢复',
4576
+ budgetExceededBadge: '已达 Token 预算上限',
4577
+ budgetLabel: 'Token 预算上限 (0 为不限制):',
4578
+ budgetWarnLabel: '预算预警阈值 (%):',
4579
+ autoCheckpointLabel: '里程碑完成时自动创建 Git 检查点',
4580
+ rollbackBtn: '回滚到该检查点',
4581
+ rollbackConfirm: '确定将工作区代码回滚至该里程碑检查点吗?',
4582
+ rollbackSuccess: '已成功回滚到检查点',
4193
4583
 
4194
4584
  goalCompleted: '目标已完成',
4195
4585
 
@@ -4366,7 +4756,35 @@ window.__ModuleLoader__.load({
4366
4756
  );
4367
4757
 
4368
4758
  // 2. Register settings card.
4369
- // Row seat first (the seat the current core renders), legacy seat kept after it.
4759
+ // List seat (plugins.item): the seat the Plugins page renders as the plugin's own
4760
+ // page with its configuration. The label is a static string on purpose — it is
4761
+ // resolved while the page renders, and a locale lookup there would take the whole
4762
+ // client batch down with it.
4763
+ registerSlotSafe(
4764
+
4765
+ 'plugins.item',
4766
+
4767
+ {
4768
+
4769
+ name: 'plugins.item',
4770
+
4771
+ id: ROW_ID,
4772
+
4773
+ order: 60,
4774
+
4775
+ label: () => 'Goal',
4776
+
4777
+ locale: NS,
4778
+
4779
+ inject: () => ({ ctx, scope: settingsScope }),
4780
+
4781
+ },
4782
+
4783
+ GoalSettingsCard,
4784
+
4785
+ );
4786
+
4787
+ // Row seat and the legacy seat kept as fallbacks.
4370
4788
 
4371
4789
  registerSlotSafe(
4372
4790
 
@@ -15,15 +15,37 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
15
15
  }
16
16
  }
17
17
 
18
+ let budgetWarningText = '';
19
+ if (snapshot.budgetWarningTriggered) {
20
+ const total = snapshot.tokensUsage?.totalTokens || 0;
21
+ const max = snapshot.maxTokenBudget || 0;
22
+ const pct = max > 0 ? Math.round((total / max) * 100) : 0;
23
+ if (lang === 'zh') {
24
+ budgetWarningText = `\n\n⚠️ TOKEN 预算预警 (已消耗 ${pct}%: ${total.toLocaleString('en-US')}/${max.toLocaleString('en-US')}):\nToken 预算即将耗尽!严禁开展额外探索,必须专注于立即完成当前里程碑并调用 goal_finish 提交成果!\n`;
25
+ } else {
26
+ budgetWarningText = `\n\n⚠️ TOKEN BUDGET ALERT (${pct}% consumed: ${total.toLocaleString('en-US')}/${max.toLocaleString('en-US')}):\nToken budget is nearing exhaustion! Avoid exploratory turns and focus strictly on completing active milestones and calling goal_finish immediately!\n`;
27
+ }
28
+ }
29
+
30
+ const formatMilestone = (m, i) => {
31
+ let line = ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`;
32
+ if (Array.isArray(m.checklist) && m.checklist.length > 0) {
33
+ const checkLines = m.checklist
34
+ .map((item) => ` [${item.done ? 'x' : ' '}] ${item.text}`)
35
+ .join('\n');
36
+ line += `\n${checkLines}`;
37
+ }
38
+ return line;
39
+ };
40
+
18
41
  if (lang === 'zh') {
19
42
  const milestonesText = hasMilestones
20
- ? snapshot.milestones
21
- .map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`)
22
- .join('\n')
43
+ ? snapshot.milestones.map(formatMilestone).join('\n')
23
44
  : ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
24
45
 
25
46
  return (
26
47
  nudgeText +
48
+ budgetWarningText +
27
49
  `\n\n[DSH GOAL MODE ACTIVE]\n` +
28
50
  `目标: "${snapshot.title}"\n` +
29
51
  `运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
@@ -31,19 +53,18 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
31
53
  `${milestonesText}\n\n` +
32
54
  `Goal Mode 执行契约 (必须严格遵循):\n` +
33
55
  `1. ${hasMilestones ? '按部就班推进当前进行中的里程碑。' : '第一步核心指令: 立即调用 goal_set_milestones 制定 3-7 个具体里程碑。在完成此工具调用前严禁执行其他操作!'}\n` +
34
- `2. 推进里程碑时,必须通过 goal_update_progress 工具更新状态(开始前标为 in_progress,完成后标为 completed 并附简要说明)。\n` +
56
+ `2. 推进里程碑时,必须通过 goal_update_progress 工具更新状态(开始前标为 in_progress,完成后标为 completed 并附简要说明)。如有细分子任务,可通过 checklist 字段同步进度。\n` +
35
57
  `3. 当所有里程碑全部完成后,调用 goal_finish 工具提交详细成果总结。`
36
58
  );
37
59
  }
38
60
 
39
61
  const milestonesText = hasMilestones
40
- ? snapshot.milestones
41
- .map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`)
42
- .join('\n')
62
+ ? snapshot.milestones.map(formatMilestone).join('\n')
43
63
  : ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
44
64
 
45
65
  return (
46
66
  nudgeText +
67
+ budgetWarningText +
47
68
  `\n\n[DSH GOAL MODE ACTIVE]\n` +
48
69
  `Goal: "${snapshot.title}"\n` +
49
70
  `Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
@@ -51,7 +72,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
51
72
  `${milestonesText}\n\n` +
52
73
  `Goal Mode Instructions (MANDATORY TO FOLLOW):\n` +
53
74
  `1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}\n` +
54
- `2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes).\n` +
75
+ `2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes). If sub-tasks exist, pass checklist array to track items.\n` +
55
76
  `3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`
56
77
  );
57
78
  }
@@ -112,3 +112,109 @@ export function exportReportGitHubPR(state) {
112
112
  md += `*Automated by [@goodandready/dsh-goal](https://github.com/GooDAnDReaDY/dsh-goal)*\n`;
113
113
  return md;
114
114
  }
115
+
116
+ import fs from 'node:fs';
117
+ import path from 'node:path';
118
+
119
+ /**
120
+ * Safely create a git milestone checkpoint
121
+ * @param {object} milestone
122
+ * @param {string} sessionId
123
+ * @param {string} [cwd]
124
+ * @returns {string|null} commit hash or tag name
125
+ */
126
+ export function createMilestoneCheckpoint(milestone, sessionId = 'default', cwd) {
127
+ if (!milestone || !milestone.id) return null;
128
+ try {
129
+ const status = execSync('git status --porcelain', {
130
+ encoding: 'utf8',
131
+ stdio: ['ignore', 'pipe', 'ignore'],
132
+ timeout: 2000,
133
+ cwd: cwd || undefined,
134
+ }).trim();
135
+
136
+ // If there are changes, auto-commit checkpoint
137
+ if (status) {
138
+ execSync('git add -A', {
139
+ stdio: ['ignore', 'ignore', 'ignore'],
140
+ timeout: 3000,
141
+ cwd: cwd || undefined,
142
+ });
143
+ const cleanTitle = (milestone.title || '').replace(/[\"\`\$]/g, '');
144
+ const msg = `checkpoint(goal): [${milestone.id}] ${cleanTitle}`;
145
+ execSync(`git commit -m "${msg}" --no-verify`, {
146
+ stdio: ['ignore', 'ignore', 'ignore'],
147
+ timeout: 5000,
148
+ cwd: cwd || undefined,
149
+ });
150
+ }
151
+
152
+ const hash = execSync('git rev-parse --short HEAD', {
153
+ encoding: 'utf8',
154
+ stdio: ['ignore', 'pipe', 'ignore'],
155
+ timeout: 1000,
156
+ cwd: cwd || undefined,
157
+ }).trim();
158
+
159
+ return hash || null;
160
+ } catch (err) {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Rollback working tree to a specific commit checkpoint
167
+ * @param {string} commitHash
168
+ * @param {string} [cwd]
169
+ * @returns {boolean}
170
+ */
171
+ export function rollbackToCheckpoint(commitHash, cwd) {
172
+ if (!commitHash || typeof commitHash !== 'string') return false;
173
+ try {
174
+ const cleanHash = commitHash.trim().replace(/[^a-zA-Z0-9_-]/g, '');
175
+ if (!cleanHash) return false;
176
+ execSync(`git checkout ${cleanHash} -- .`, {
177
+ stdio: ['ignore', 'ignore', 'ignore'],
178
+ timeout: 5000,
179
+ cwd: cwd || undefined,
180
+ });
181
+ return true;
182
+ } catch (err) {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Save full goal execution artifact markdown file into .dsh/goals/
189
+ * @param {object} state
190
+ * @param {string} [cwd]
191
+ * @returns {{ relativePath: string, fullPath: string, content: string }|null}
192
+ */
193
+ export function saveGoalArtifact(state, cwd) {
194
+ if (!state || !state.title) return null;
195
+ try {
196
+ const root = cwd || process.cwd();
197
+ const targetDir = path.join(root, '.dsh', 'goals');
198
+ if (!fs.existsSync(targetDir)) {
199
+ fs.mkdirSync(targetDir, { recursive: true });
200
+ }
201
+
202
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
203
+ const safeTitle = (state.title || 'goal')
204
+ .toLowerCase()
205
+ .replace(/[^a-z0-9]+/g, '-')
206
+ .replace(/^-+|-+$/g, '')
207
+ .slice(0, 40) || 'run';
208
+
209
+ const filename = `${timestamp}-${safeTitle}.md`;
210
+ const fullPath = path.join(targetDir, filename);
211
+ const relativePath = path.join('.dsh', 'goals', filename).replace(/\\/g, '/');
212
+
213
+ const content = exportReportMarkdown(state);
214
+ fs.writeFileSync(fullPath, content, 'utf8');
215
+
216
+ return { relativePath, fullPath, content };
217
+ } catch (err) {
218
+ return null;
219
+ }
220
+ }
@@ -1,10 +1,21 @@
1
1
  import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
2
- import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR } from './engine-reports.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
+ function matchMilestone(m, id) {
6
+ if (!m || id === undefined || id === null) return false;
7
+ if (m.id === String(id)) return true;
8
+ const s1 = String(m.id).toLowerCase().replace(/[^a-z0-9]/g, '');
9
+ const s2 = String(id).toLowerCase().replace(/[^a-z0-9]/g, '');
10
+ if (s1 && s1 === s2) return true;
11
+ const n1 = s1.replace(/^m+/, '');
12
+ const n2 = s2.replace(/^m+/, '');
13
+ return Boolean(n1 && n1 === n2);
14
+ }
15
+
5
16
 
6
17
  export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf };
7
- export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR };
18
+ export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
8
19
 
9
20
  /**
10
21
  * Isolated Goal Management Engine for DeepSeek Harness
@@ -16,6 +27,9 @@ export class GoalEngine {
16
27
  this.enableSound = options.enableSound ?? true;
17
28
  this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
18
29
  this.consecutiveToolFailureLimit = options.consecutiveToolFailureLimit ?? 3;
30
+ this.maxTokenBudget = options.maxTokenBudget ?? 0;
31
+ this.budgetWarningThreshold = options.budgetWarningThreshold ?? 80;
32
+ this.autoCheckpointOnMilestone = options.autoCheckpointOnMilestone ?? false;
19
33
  this.maxSessions = options.maxSessions ?? 100;
20
34
  this.goals = new Map();
21
35
  this.listeners = new Set();
@@ -99,6 +113,15 @@ export class GoalEngine {
99
113
  if (typeof config.consecutiveToolFailureLimit === 'number') {
100
114
  this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
101
115
  }
116
+ if (typeof config.maxTokenBudget === 'number') {
117
+ this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
118
+ }
119
+ if (typeof config.budgetWarningThreshold === 'number') {
120
+ this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
121
+ }
122
+ if (typeof config.autoCheckpointOnMilestone === 'boolean') {
123
+ this.autoCheckpointOnMilestone = config.autoCheckpointOnMilestone;
124
+ }
102
125
  this.emit();
103
126
  }
104
127
 
@@ -218,6 +241,8 @@ export class GoalEngine {
218
241
  completedAt: null,
219
242
  iterationsCount: 0,
220
243
  maxIterations: options.maxIterations ?? this.defaultMaxIterations,
244
+ maxTokenBudget: options.maxTokenBudget ?? this.maxTokenBudget,
245
+ budgetWarningTriggered: false,
221
246
  gitStartCommit: gitCommit || null,
222
247
  pendingNudge: null,
223
248
  nudges: [],
@@ -377,14 +402,15 @@ export class GoalEngine {
377
402
  if (shouldEmit) this.emit(sid);
378
403
  }
379
404
 
380
- updateMilestone(id, status, notes = '', sessionId = 'default') {
405
+ updateMilestone(id, status, notes = '', sessionId = 'default', checklist = null) {
381
406
  const sid = sessionId || 'default';
382
407
  const goal = this.goals.get(sid);
383
408
  if (!goal) return false;
384
409
 
385
- const target = goal.milestones.find((m) => m.id === String(id));
410
+ const target = goal.milestones.find((m) => matchMilestone(m, id));
386
411
  if (!target) return false;
387
412
 
413
+ const prevStatus = target.status;
388
414
  if (status && Object.values(MilestoneStatus).includes(status)) {
389
415
  target.status = status;
390
416
  }
@@ -392,6 +418,26 @@ export class GoalEngine {
392
418
  target.notes = String(notes);
393
419
  }
394
420
 
421
+ if (Array.isArray(checklist)) {
422
+ target.checklist = checklist.map((item) => ({
423
+ text: String(item.text || item.title || '').trim(),
424
+ done: Boolean(item.done || item.completed),
425
+ })).filter((item) => item.text.length > 0);
426
+ }
427
+
428
+ // Auto Git Checkpoint on milestone completion
429
+ if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
430
+ const commitHash = createMilestoneCheckpoint(target, sid);
431
+ if (commitHash) {
432
+ target.checkpointCommit = commitHash;
433
+ goal.logs.push({
434
+ timestamp: Date.now(),
435
+ type: 'info',
436
+ message: `Git checkpoint saved for [${target.title}]: ${commitHash}`,
437
+ });
438
+ }
439
+ }
440
+
395
441
  goal.logs.push({
396
442
  timestamp: Date.now(),
397
443
  type: 'milestone',
@@ -407,6 +453,50 @@ export class GoalEngine {
407
453
  return true;
408
454
  }
409
455
 
456
+ toggleChecklistItem(milestoneId, itemIndex, done, sessionId = 'default') {
457
+ const sid = sessionId || 'default';
458
+ const goal = this.goals.get(sid);
459
+ if (!goal) return false;
460
+
461
+ const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
462
+ if (!target || !Array.isArray(target.checklist) || !target.checklist[itemIndex]) {
463
+ return false;
464
+ }
465
+
466
+ target.checklist[itemIndex].done = done !== undefined ? Boolean(done) : !target.checklist[itemIndex].done;
467
+ this.recordProgress(sid);
468
+ this.emit(sid);
469
+ return true;
470
+ }
471
+
472
+ extendBudget(addTokens = 50000, sessionId = 'default') {
473
+ const sid = sessionId || 'default';
474
+ const goal = this.goals.get(sid);
475
+ if (!goal) return false;
476
+
477
+ const currentTotal = goal.tokensUsage?.totalTokens || 0;
478
+ const currentMax = goal.maxTokenBudget || this.maxTokenBudget || 0;
479
+ const newBudget = Math.max(currentTotal, currentMax) + Math.max(1000, Number(addTokens) || 50000);
480
+
481
+ goal.maxTokenBudget = newBudget;
482
+ goal.budgetWarningTriggered = false;
483
+
484
+ if (goal.state === GoalState.PAUSED && goal.logs?.some((l) => l.message?.includes('Token budget reached'))) {
485
+ goal.state = GoalState.RUNNING;
486
+ goal.pausedAt = null;
487
+ }
488
+
489
+ goal.logs.push({
490
+ timestamp: Date.now(),
491
+ type: 'info',
492
+ message: `Token budget extended to ${newBudget.toLocaleString('en-US')} tokens`,
493
+ });
494
+ if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
495
+
496
+ this.emit(sid, true);
497
+ return true;
498
+ }
499
+
410
500
  incrementIteration(sessionId = 'default') {
411
501
  const sid = sessionId || 'default';
412
502
  const goal = this.goals.get(sid);
@@ -462,6 +552,36 @@ export class GoalEngine {
462
552
  goal.tokensUsage.completionTokens += completion;
463
553
  goal.tokensUsage.totalTokens += total;
464
554
 
555
+ // Token Budget Guard Check
556
+ const budget = goal.maxTokenBudget || this.maxTokenBudget || 0;
557
+ if (budget > 0) {
558
+ const currentTotal = goal.tokensUsage.totalTokens;
559
+ const warnThreshold = (budget * (this.budgetWarningThreshold || 80)) / 100;
560
+
561
+ if (currentTotal >= warnThreshold && !goal.budgetWarningTriggered) {
562
+ goal.budgetWarningTriggered = true;
563
+ goal.logs.push({
564
+ timestamp: Date.now(),
565
+ type: 'warning',
566
+ message: `Token budget warning: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens consumed (${Math.round((currentTotal / budget) * 100)}%)`,
567
+ });
568
+ if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
569
+ }
570
+
571
+ if (currentTotal >= budget && goal.state === GoalState.RUNNING) {
572
+ goal.state = GoalState.PAUSED;
573
+ goal.pausedAt = Date.now();
574
+ goal.logs.push({
575
+ timestamp: Date.now(),
576
+ type: 'warning',
577
+ message: `Token budget reached: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens. Pausing autonomous loop.`,
578
+ });
579
+ if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
580
+ this.emit(sid, true);
581
+ return;
582
+ }
583
+ }
584
+
465
585
  this.emit(sid);
466
586
  }
467
587
 
@@ -514,6 +634,10 @@ export class GoalEngine {
514
634
  pendingNudge: null,
515
635
  toolFailureCount: 0,
516
636
  consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
637
+ maxTokenBudget: this.maxTokenBudget,
638
+ budgetWarningThreshold: this.budgetWarningThreshold,
639
+ budgetWarningTriggered: false,
640
+ autoCheckpointOnMilestone: this.autoCheckpointOnMilestone,
517
641
  };
518
642
  }
519
643
 
@@ -553,6 +677,10 @@ export class GoalEngine {
553
677
  pendingNudge: goal.pendingNudge || null,
554
678
  toolFailureCount: this.getToolFailureCount(sid),
555
679
  consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
680
+ maxTokenBudget: goal.maxTokenBudget ?? this.maxTokenBudget,
681
+ budgetWarningThreshold: this.budgetWarningThreshold,
682
+ budgetWarningTriggered: Boolean(goal.budgetWarningTriggered),
683
+ autoCheckpointOnMilestone: this.autoCheckpointOnMilestone,
556
684
  };
557
685
  }
558
686
 
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,5 @@
1
1
  import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
2
+ import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
2
3
 
3
4
  /**
4
5
  * Register webServer HTTP routes and SSE stream for DSH Goal
@@ -247,7 +248,7 @@ export function registerRoutes(ctx, {
247
248
  res.statusCode = 400;
248
249
  return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
249
250
  }
250
- const ok = engine.updateMilestone(milestoneId, status, notes, sid);
251
+ const ok = engine.updateMilestone(milestoneId, status, notes, sid, data.checklist);
251
252
  if (!ok) {
252
253
  res.statusCode = 404;
253
254
  return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
@@ -256,6 +257,64 @@ export function registerRoutes(ctx, {
256
257
  break;
257
258
  }
258
259
 
260
+ case 'toggle_checklist_item': {
261
+ if (!milestoneId || data.itemIndex === undefined) {
262
+ res.statusCode = 400;
263
+ return res.end(JSON.stringify({ error: 'milestoneId and itemIndex are required' }));
264
+ }
265
+ const ok = engine.toggleChecklistItem(milestoneId, data.itemIndex, data.done, sid);
266
+ if (!ok) {
267
+ res.statusCode = 404;
268
+ return res.end(JSON.stringify({ error: `Milestone checklist item not found` }));
269
+ }
270
+ result = engine.getSnapshot(sid);
271
+ break;
272
+ }
273
+
274
+ case 'extend_budget': {
275
+ const addTokens = data.addTokens || 50000;
276
+ const ok = engine.extendBudget(addTokens, sid);
277
+ if (!ok) {
278
+ res.statusCode = 404;
279
+ return res.end(JSON.stringify({ error: 'No active goal found to extend budget' }));
280
+ }
281
+ if (typeof resumeActiveAgent === 'function') {
282
+ resumeActiveAgent(undefined, sid);
283
+ }
284
+ result = engine.getSnapshot(sid);
285
+ break;
286
+ }
287
+
288
+ case 'rollback_milestone': {
289
+ const commit = data.commit || data.checkpointCommit;
290
+ if (!commit) {
291
+ res.statusCode = 400;
292
+ return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
293
+ }
294
+ const ok = rollbackToCheckpoint(commit, data.cwd);
295
+ if (!ok) {
296
+ res.statusCode = 500;
297
+ return res.end(JSON.stringify({ error: `Failed to rollback to checkpoint ${commit}` }));
298
+ }
299
+ result = engine.getSnapshot(sid);
300
+ break;
301
+ }
302
+
303
+ case 'save_artifact': {
304
+ const snap = engine.getSnapshot(sid);
305
+ if (!snap || !snap.hasActiveGoal) {
306
+ res.statusCode = 404;
307
+ return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
308
+ }
309
+ const artifact = saveGoalArtifact(snap, data.cwd);
310
+ if (!artifact) {
311
+ res.statusCode = 500;
312
+ return res.end(JSON.stringify({ error: 'Failed to write goal artifact to disk' }));
313
+ }
314
+ res.statusCode = 200;
315
+ return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
316
+ }
317
+
259
318
  default:
260
319
  res.statusCode = 400;
261
320
  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.4",
3
+ "version": "0.2.6",
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",