@goodandready/dsh-goal 0.2.5 → 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 +13 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/client.js +393 -3
- package/lib/engine-prompt.js +29 -8
- package/lib/engine-reports.js +106 -0
- package/lib/goal-engine.js +132 -4
- package/lib/index.js +15 -0
- package/lib/routes.js +60 -1
- package/lib/tools.js +19 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
|
3646
|
+
const budgetStatus = getFieldStatus('maxTokenBudget', draft?.maxTokenBudget, snap);
|
|
3647
|
+
|
|
3648
|
+
const budgetWarnStatus = getFieldStatus('budgetWarningThreshold', draft?.budgetWarningThreshold, snap);
|
|
3390
3649
|
|
|
3391
|
-
const
|
|
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;
|
|
3653
|
+
|
|
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
|
|
package/lib/engine-prompt.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/engine-reports.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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