@goodandready/dsh-goal 0.2.27 → 0.2.28

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
@@ -137,6 +137,14 @@ Handles `/goal` commands and subcommands:
137
137
  * **Plugin Settings Card**: Schemastery-backed settings UI registered via `settings.plugin.item`.
138
138
 
139
139
  ### 5. Two-Phase Goal Execution: Plan Mode ⇄ Act Mode (#162)
140
+
141
+ ### 6. Acceptance Gate & Critic Verification (#163)
142
+ * **Automated Phase Verification**: Attach an optional `verificationCommand` (e.g. `npm test`, `pytest`, `cargo test`) to any milestone.
143
+ * **Failure Blocking & Diagnostic Feedback**: When the agent attempts to complete a milestone via `goal_milestones action: 'complete'` or `goal_update_progress status: 'completed'`, the command executes inside confined `safeCwd`. If tests fail (`exitCode !== 0`), completion is blocked, the milestone remains open, and diagnostic failure output is returned as actionable feedback to the agent.
144
+ * **Atomic Completion & Snapshots**: Git checkpoint snapshotting and issue checklist synchronization only trigger once the verification command passes with exit code 0.
145
+ * **UI Visual Badges**: Visual indicator `🛡️ <command>` and failure alerts rendered in the Goal Details modal.
146
+
147
+
140
148
  * **Plan Mode (Read-Only Planning Phase)**: The agent explores the codebase, searches the web, and decomposes tasks into structured milestones and checklists. All file mutating tools (`write_to_file`, `replace_file_content`, mutating shell commands) are strictly blocked by `lib/tool-guard.js`.
141
149
  * **Act Mode (Implementation Phase)**: The operator reviews and approves the plan directly via the banner or details modal with «Approve & Execute», unlocking mutating capabilities and resuming the agent.
142
150
  * **REST API & Quick Launch**: Toggle modes on demand via `POST /dsh-goal/action` (`set_mode`, `approve_plan`), or select initial mode directly in the Quick Launch dialog.
package/README.ru.md CHANGED
@@ -54,6 +54,7 @@
54
54
  * 🔄 **Перехват и замещение инструментов ядра DSH (*Добавлено в v0.1.10*)**: Бесшовная совместимость для моделей, вызывающих стандартные инструменты DSH (`update_goal`, `get_goal`, `create_goal`). Перехватывает действия (`complete`, `pause`, `resume`, `edit`, `blocked`), устраняет падения ядра (`complete and blocked require a direct human turn`) и синхронизирует состояние напрямую с GoalEngine.
55
55
  * 🛡️ **Защитные лимиты**: настраиваемое ограничение `maxIterations` для предотвращения бесконечных циклов.
56
56
  * 🛡️ **Двухфазный режим «Plan Mode ⇄ Act Mode» (*#162*)**: Разделение жизненного цикла цели на фазу проектирования и планирования (только чтение и формирование этапов через `goal_milestones`, блокировка записи файлов и мутирующих команд) и фазу реализации (`Act Mode`), активируемую по кнопке «Утвердить план и запустить» (Approve & Execute).
57
+ * 🛡️ **Автоматический приёмочный гейт этапа (Critic Verification) (*#163*)**: Опциональное поле `verificationCommand` на этапе (например, `npm test`, `pytest`). При попытке закрыть этап команда автоматически запускается в изолированном `safeCwd`. В случае сбоя (`exitCode != 0`) этап остаётся открытым, а диагностический вывод возвращается модели в качестве обратной связи. Git-снимки и синхронизация задач создаются только при коде возврата 0.
57
58
 
58
59
  ---
59
60
 
package/README.zh.md CHANGED
@@ -55,6 +55,7 @@
55
55
  * 🛡️ **安全防护机制**:支持自定义最大迭代次数(`maxIterations`)以及智能卡顿检测(Smart Progress Guard)。
56
56
  * 🔔 **Web Audio 提示音**:任务完成或失败时,通过 Web Audio API 播放舒缓的合成音效。
57
57
  * 📋 **两阶段目标模式:Plan Mode ⇄ Act Mode (*#162*)**:分离规划设计与落地执行。只读规划阶段严格限制文件变更与危险命令,待操作者在横幅或弹窗点击“批准并执行 (Approve & Execute)”后切入执行阶段。
58
+ * 🛡️ **自动化验收门禁与评审检验 (Critic Verification) (*#163*)**:里程碑可配置 `verificationCommand` 验证命令(如 `npm test`、`pytest`)。智能体尝试关闭阶段时,在受控 `safeCwd` 中自动执行检查;若失败(`exitCode != 0`)则阻断关闭并向模型反馈错误输出,仅当命令返回 0 时方可进入完成态并创建 Git 检查点。
58
59
 
59
60
  ---
60
61
 
package/lib/client.js CHANGED
@@ -1640,6 +1640,29 @@ window.__ModuleLoader__.load({
1640
1640
  margin-left: 6px;
1641
1641
  }
1642
1642
 
1643
+ .dsh-goal-gate-badge {
1644
+ font-size: 10px;
1645
+ padding: 1px 6px;
1646
+ border-radius: 4px;
1647
+ background: var(--dsw-alias-bg-layer-4);
1648
+ color: var(--dsw-alias-brand-primary);
1649
+ border: 1px solid var(--dsw-alias-brand-primary);
1650
+ margin-left: 6px;
1651
+ display: inline-flex;
1652
+ align-items: center;
1653
+ gap: 3px;
1654
+ }
1655
+
1656
+ .dsh-goal-gate-error {
1657
+ font-size: 11px;
1658
+ color: var(--dsw-alias-state-danger-primary);
1659
+ margin-top: 3px;
1660
+ padding: 2px 6px;
1661
+ border-radius: 3px;
1662
+ background: var(--dsw-alias-bg-layer-2);
1663
+ font-family: monospace;
1664
+ }
1665
+
1643
1666
  `;
1644
1667
 
1645
1668
  function injectStylesOnce() {
@@ -2378,16 +2401,33 @@ window.__ModuleLoader__.load({
2378
2401
  },
2379
2402
 
2380
2403
  `${i + 1}. ${m.title}`,
2381
- Array.isArray(m.dependsOn) && m.dependsOn.some((depId) => {
2382
- const prereq = milestones.find((x) => x.id === depId || x.title === depId);
2383
- return prereq && prereq.status !== 'completed';
2384
- })
2385
- ? React.createElement('span', { className: 'dsh-goal-lock-badge' }, `🔒 ${t('waitingPrereq') || 'waiting for prereq'}`)
2386
- : null,
2404
+ m.verificationCommand
2405
+ ? React.createElement(
2406
+ 'span',
2407
+ {
2408
+ className: 'dsh-goal-gate-badge',
2409
+ title: `${t('verificationGate') || 'Acceptance gate'}: ${m.verificationCommand}`,
2410
+ },
2411
+ `🛡️ ${m.verificationCommand}`
2412
+ )
2413
+ : null,
2414
+ Array.isArray(m.dependsOn) && m.dependsOn.some((depId) => {
2415
+ const prereq = milestones.find((x) => x.id === depId || x.title === depId);
2416
+ return prereq && prereq.status !== 'completed';
2417
+ })
2418
+ ? React.createElement('span', { className: 'dsh-goal-lock-badge' }, `🔒 ${t('waitingPrereq') || 'waiting for prereq'}`)
2419
+ : null,
2387
2420
 
2388
2421
  ),
2389
2422
 
2390
2423
  m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
2424
+ m.verificationError
2425
+ ? React.createElement(
2426
+ 'div',
2427
+ { className: 'dsh-goal-gate-error' },
2428
+ `⚠️ ${t('verificationFailed') || 'Gate failed'}: ${m.verificationError}`
2429
+ )
2430
+ : null,
2391
2431
 
2392
2432
  Array.isArray(m.checklist) && m.checklist.length > 0
2393
2433
  ? React.createElement(
@@ -5041,6 +5081,9 @@ window.__ModuleLoader__.load({
5041
5081
  mergeBranch: 'Merge Branch',
5042
5082
  discardBranch: 'Discard Branch',
5043
5083
  retrospectiveTitle: 'Goal Retrospective',
5084
+ verificationGate: 'Acceptance gate',
5085
+ verificationPassed: 'Verified',
5086
+ verificationFailed: 'Gate failed',
5044
5087
  templatesTitle: 'Goal Templates',
5045
5088
  waitingPrereq: 'waiting for prereq',
5046
5089
 
@@ -5189,6 +5232,9 @@ window.__ModuleLoader__.load({
5189
5232
  mergeBranch: '合并分支',
5190
5233
  discardBranch: '放弃分支',
5191
5234
  retrospectiveTitle: '目标复盘报告',
5235
+ verificationGate: '验收门禁',
5236
+ verificationPassed: '已验证',
5237
+ verificationFailed: '门禁未通过',
5192
5238
  templatesTitle: '目标模板',
5193
5239
  waitingPrereq: '等待前置依赖',
5194
5240
 
@@ -74,11 +74,13 @@ export function parseMilestoneItems(milestonesList, startingCount = 0) {
74
74
 
75
75
  count += 1;
76
76
  const mId = typeof item === 'object' && item?.id ? item.id : ('m-' + count);
77
+ const vCmd = typeof item === 'object' && (item?.verificationCommand || item?.verification_command);
77
78
  const mObj = {
78
79
  id: String(mId),
79
80
  title: itemTitle.trim(),
80
81
  status: typeof item === 'object' && item?.status ? item.status : MilestoneStatus.PENDING,
81
82
  notes: typeof item === 'object' && item?.notes ? String(item.notes) : '',
83
+ verificationCommand: vCmd ? String(vCmd).trim() : null,
82
84
  dependsOn: typeof item === 'object' && Array.isArray(item?.dependsOn) ? item.dependsOn.map(String) : [],
83
85
  };
84
86
  if (typeof item === 'object' && Array.isArray(item?.checklist)) {
@@ -116,6 +118,10 @@ export function applyMilestoneUpdate(target, status, notes = '', checklist = nul
116
118
  }
117
119
  }
118
120
 
121
+ if (options.verificationCommand !== undefined) {
122
+ target.verificationCommand = options.verificationCommand ? String(options.verificationCommand).trim() : null;
123
+ }
124
+
119
125
  if (status) {
120
126
  if (validStatuses.length > 0 && !validStatuses.includes(status)) {
121
127
  return {
@@ -0,0 +1,129 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { getSafeCwd } from './goal-engine-constants.js';
3
+
4
+ /**
5
+ * Execute a verification command in a confined working directory with timeout and output capture
6
+ * @param {string} command Shell command to execute
7
+ * @param {string} [targetCwd] Working directory
8
+ * @param {object} [options]
9
+ * @param {number} [options.timeoutMs=30000] Timeout in milliseconds
10
+ * @param {number} [options.maxBuffer=1048576] Max stdout/stderr buffer in bytes (1MB)
11
+ * @param {object} [options.env={}] Additional environment variables
12
+ * @returns {{ ok: boolean, exitCode: number, stdout: string, stderr: string, error?: string }}
13
+ */
14
+ export function runMilestoneVerification(command, targetCwd, options = {}) {
15
+ if (!command || typeof command !== 'string' || !command.trim()) {
16
+ return {
17
+ ok: false,
18
+ exitCode: 1,
19
+ stdout: '',
20
+ stderr: '',
21
+ error: 'Empty or invalid verification command',
22
+ };
23
+ }
24
+
25
+ const safeCwd = getSafeCwd(targetCwd) || process.cwd();
26
+ const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 30000;
27
+ const maxBuffer = typeof options.maxBuffer === 'number' ? options.maxBuffer : 1024 * 1024;
28
+
29
+ try {
30
+ const res = spawnSync(command.trim(), {
31
+ shell: true,
32
+ cwd: safeCwd,
33
+ timeout: timeoutMs,
34
+ maxBuffer,
35
+ encoding: 'utf8',
36
+ env: { ...process.env, ...options.env },
37
+ });
38
+
39
+ if (res.error) {
40
+ if (res.error.code === 'ETIMEDOUT') {
41
+ return {
42
+ ok: false,
43
+ exitCode: 124,
44
+ stdout: res.stdout || '',
45
+ stderr: res.stderr || '',
46
+ error: `Verification command timed out after ${timeoutMs}ms`,
47
+ };
48
+ }
49
+ return {
50
+ ok: false,
51
+ exitCode: res.status ?? 1,
52
+ stdout: res.stdout || '',
53
+ stderr: res.stderr || '',
54
+ error: res.error.message || 'Verification command failed to execute',
55
+ };
56
+ }
57
+
58
+ if (res.status !== 0) {
59
+ const output = (res.stderr || res.stdout || '').trim();
60
+ return {
61
+ ok: false,
62
+ exitCode: res.status ?? 1,
63
+ stdout: res.stdout || '',
64
+ stderr: res.stderr || '',
65
+ error: `Verification command failed with exit code ${res.status}${output ? ': ' + output.slice(0, 500) : ''}`,
66
+ };
67
+ }
68
+
69
+ return {
70
+ ok: true,
71
+ exitCode: 0,
72
+ stdout: res.stdout || '',
73
+ stderr: res.stderr || '',
74
+ };
75
+ } catch (err) {
76
+ return {
77
+ ok: false,
78
+ exitCode: 1,
79
+ stdout: '',
80
+ stderr: '',
81
+ error: err?.message || 'Unexpected verification failure',
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Run acceptance gate check on a milestone target and record diagnostic logs/activities
88
+ * @param {object} target Milestone object
89
+ * @param {string} targetCwd Working directory
90
+ * @param {object} [goal] Goal engine state
91
+ * @param {Function} [recordActivity] Activity recorder function
92
+ * @param {object} [options]
93
+ * @returns {{ ok: boolean, verified: boolean, error?: string, output?: string, passedAt?: number }}
94
+ */
95
+ export function verifyMilestoneAcceptance(target, targetCwd, goal = null, recordActivity = null, options = {}) {
96
+ if (!target || !target.verificationCommand) {
97
+ return { ok: true, verified: false };
98
+ }
99
+
100
+ const vRes = runMilestoneVerification(target.verificationCommand, targetCwd, options);
101
+ if (!vRes.ok) {
102
+ target.verificationError = vRes.error;
103
+ target.verificationOutput = (vRes.stderr || vRes.stdout || '').trim();
104
+ if (goal) {
105
+ if (typeof recordActivity === 'function') {
106
+ recordActivity(goal, {
107
+ type: 'milestone',
108
+ tool: 'updateMilestone',
109
+ status: 'error',
110
+ details: `Verification failed: ${vRes.error}`,
111
+ });
112
+ }
113
+ if (Array.isArray(goal.logs)) {
114
+ goal.logs.push({
115
+ timestamp: Date.now(),
116
+ type: 'error',
117
+ message: `Acceptance gate failed for [${target.title}]: ${vRes.error}`,
118
+ });
119
+ if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
120
+ }
121
+ }
122
+ return { ok: false, verified: true, error: vRes.error, output: target.verificationOutput };
123
+ }
124
+
125
+ target.verificationError = null;
126
+ target.verificationOutput = null;
127
+ target.verificationPassedAt = Date.now();
128
+ return { ok: true, verified: true, passedAt: target.verificationPassedAt };
129
+ }
@@ -1,3 +1,4 @@
1
+ import { verifyMilestoneAcceptance } from './engine-verification.js';
1
2
  import { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot, getSafeCwd } from './goal-engine-constants.js';
2
3
  import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
3
4
  import { EngineStore } from './engine-store.js';
@@ -357,21 +358,22 @@ export class GoalEngine {
357
358
  const target = goal.milestones.find((m) => matchMilestone(m, id));
358
359
  if (!target) return false;
359
360
 
361
+ if (status === MilestoneStatus.COMPLETED && target.verificationCommand) {
362
+ const vRes = verifyMilestoneAcceptance(target, goal.cwd || process.cwd(), goal, recordActivity);
363
+ if (!vRes.ok) {
364
+ this.emit(sid);
365
+ return false;
366
+ }
367
+ }
368
+
360
369
  const res = applyMilestoneUpdate(target, status, notes, checklist, Object.values(MilestoneStatus), {
361
370
  enforceDependencies: this.enableMilestoneDependencies,
362
371
  allMilestones: goal.milestones,
363
372
  });
364
373
  if (res.invalidStatus || res.blocked) {
365
- if (res.invalidStatus) {
366
- this.logger?.warn?.(`[dsh-goal] Invalid milestone status "${status}" for milestone ${id}`);
367
- }
374
+ if (res.invalidStatus) this.logger?.warn?.(`[dsh-goal] Invalid milestone status "${status}" for milestone ${id}`);
368
375
  if (res.blocked) {
369
- recordActivity(goal, {
370
- type: 'milestone',
371
- tool: 'updateMilestone',
372
- status: 'error',
373
- details: `Milestone ${id} blocked by ${res.missingDependencies?.join(', ')}`,
374
- });
376
+ recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'error', details: `Milestone ${id} blocked by ${res.missingDependencies?.join(', ')}` });
375
377
  }
376
378
  return false;
377
379
  }
@@ -1,4 +1,5 @@
1
1
  import { syncMilestoneWithIssue } from './engine-issue-sync.js';
2
+ import { runMilestoneVerification } from './engine-verification.js';
2
3
 
3
4
  /**
4
5
  * Milestone and checklist coordinator for active core goals
@@ -24,6 +25,7 @@ export class MilestoneManager {
24
25
  completed: Boolean(m.completed),
25
26
  completedAt: m.completedAt || null,
26
27
  notes: m.notes || '',
28
+ verificationCommand: m.verificationCommand || m.verification_command ? String(m.verificationCommand || m.verification_command).trim() : null,
27
29
  dependsOn: Array.isArray(m.dependsOn) ? m.dependsOn : [],
28
30
  checklist: Array.isArray(m.checklist) ? m.checklist.map((c, cIdx) => ({
29
31
  id: c.id || `cl-${idx + 1}-${cIdx + 1}`,
@@ -35,16 +37,18 @@ export class MilestoneManager {
35
37
  return sanitized;
36
38
  }
37
39
 
38
- addMilestone(goalId, { title, checklist = [], dependsOn = [] }) {
40
+ addMilestone(goalId, { title, checklist = [], dependsOn = [], verificationCommand = null, verification_command = null }) {
39
41
  if (!goalId || !this.store) throw new Error('Missing goalId or store');
40
42
  const list = this.getMilestones(goalId);
41
43
  const id = `m-${list.length + 1}`;
44
+ const vCmd = verificationCommand || verification_command;
42
45
  const newM = {
43
46
  id,
44
47
  title: title || `Milestone ${list.length + 1}`,
45
48
  completed: false,
46
49
  completedAt: null,
47
50
  notes: '',
51
+ verificationCommand: vCmd ? String(vCmd).trim() : null,
48
52
  dependsOn: Array.isArray(dependsOn) ? dependsOn : [],
49
53
  checklist: (checklist || []).map((c, cIdx) => ({
50
54
  id: `cl-${list.length + 1}-${cIdx + 1}`,
@@ -74,6 +78,20 @@ export class MilestoneManager {
74
78
  }
75
79
  }
76
80
 
81
+ // Acceptance Gate (Critic Verification) - Issue #163
82
+ if (m.verificationCommand) {
83
+ const vResult = runMilestoneVerification(m.verificationCommand, workspaceRoot);
84
+ if (!vResult.ok) {
85
+ m.verificationError = vResult.error;
86
+ m.verificationOutput = (vResult.stderr || vResult.stdout || '').trim();
87
+ this.store.setMilestones(goalId, list);
88
+ throw new Error(`Acceptance gate failed for milestone "${milestoneId}" (${m.verificationCommand}): ${vResult.error}`);
89
+ }
90
+ m.verificationError = null;
91
+ m.verificationOutput = null;
92
+ m.verificationPassedAt = Date.now();
93
+ }
94
+
77
95
  m.completed = true;
78
96
  m.completedAt = Date.now();
79
97
  if (notes) m.notes = notes;
@@ -110,6 +128,33 @@ export class MilestoneManager {
110
128
  return { milestone: m, snapshot };
111
129
  }
112
130
 
131
+ updateMilestone(goalId, milestoneId, { status, notes = '', checklist = null, verificationCommand = null, cwd = null } = {}) {
132
+ if (!goalId || !milestoneId) throw new Error('goalId and milestoneId are required');
133
+ const list = this.getMilestones(goalId);
134
+ const m = list.find(item => item.id === milestoneId);
135
+ if (!m) return null;
136
+
137
+ if (verificationCommand !== undefined && verificationCommand !== null) {
138
+ m.verificationCommand = String(verificationCommand).trim() || null;
139
+ }
140
+
141
+ if (status === 'completed') {
142
+ return this.completeMilestone(goalId, milestoneId, notes, cwd);
143
+ }
144
+
145
+ if (status) m.status = status;
146
+ if (notes) m.notes = notes;
147
+ if (Array.isArray(checklist)) {
148
+ m.checklist = checklist.map((c, cIdx) => ({
149
+ id: typeof c === 'object' && c?.id ? c.id : `cl-${milestoneId}-${cIdx + 1}`,
150
+ text: typeof c === 'string' ? c : (c?.text || ''),
151
+ done: Boolean(c?.done),
152
+ }));
153
+ }
154
+ this.store.setMilestones(goalId, list);
155
+ return m;
156
+ }
157
+
113
158
  toggleChecklist(goalId, milestoneId, index, done) {
114
159
  if (!goalId || !milestoneId) throw new Error('goalId and milestoneId are required');
115
160
  const list = this.getMilestones(goalId);
@@ -139,7 +184,8 @@ export class MilestoneManager {
139
184
  if (uncompletedChecklist.length > 0) {
140
185
  clText = ` | Next items: ${uncompletedChecklist.map(c => `[ ] ${c.text}`).join(', ')}`;
141
186
  }
187
+ const gateText = active.verificationCommand ? ` [Gate: "${active.verificationCommand}"]` : '';
142
188
 
143
- return `[Goal Milestones]: Current step: "${active.title}"${clText}`;
189
+ return `[Goal Milestones]: Current step: "${active.title}"${gateText}${clText}`;
144
190
  }
145
191
  }
package/lib/tools.js CHANGED
@@ -76,6 +76,7 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
76
76
  title: args.title,
77
77
  checklist: args.checklist,
78
78
  dependsOn: args.dependsOn || args.depends_on,
79
+ verificationCommand: args.verificationCommand || args.verification_command,
79
80
  });
80
81
  return { ok: true, goalId, milestone: item };
81
82
  }
@@ -124,6 +125,7 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
124
125
  },
125
126
  },
126
127
  dependsOn: { type: 'array', items: { type: 'string' } },
128
+ verificationCommand: { type: 'string', description: 'Optional shell command to verify milestone completion' },
127
129
  },
128
130
  required: ['title'],
129
131
  },
@@ -136,6 +138,10 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
136
138
  items: { type: 'string' },
137
139
  description: 'Initial checklist items for added milestone',
138
140
  },
141
+ verificationCommand: {
142
+ type: 'string',
143
+ description: 'Verification command when action is "add"',
144
+ },
139
145
  },
140
146
  required: ['action'],
141
147
  },
@@ -230,7 +236,12 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
230
236
  const completed = await mm.completeMilestone(goalId, milestoneId, notes, safeCwd);
231
237
  return { ok: true, goalId, milestoneId, status: 'completed', milestone: completed };
232
238
  }
233
- const updated = mm.updateMilestone(goalId, milestoneId, { status, notes, checklist: args.checklist });
239
+ const updated = mm.updateMilestone(goalId, milestoneId, {
240
+ status,
241
+ notes,
242
+ checklist: args.checklist,
243
+ verificationCommand: args.verificationCommand || args.verification_command,
244
+ });
234
245
  return { ok: true, goalId, milestoneId, status, milestone: updated };
235
246
  };
236
247
 
@@ -244,6 +255,7 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
244
255
  status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'blocked'], description: 'Milestone status' },
245
256
  notes: { type: 'string', description: 'Brief outcome or status notes' },
246
257
  checklist: { type: 'array', items: { type: 'string' }, description: 'Subtask items' },
258
+ verificationCommand: { type: 'string', description: 'Optional verification command to set or update' },
247
259
  },
248
260
  required: ['milestone_id', 'status'],
249
261
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Core Goal Addon (Token Budget, Milestones, Git Snapshots & Retrospective Reports) for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,10 +18,6 @@
18
18
  "cordis.patch.yml",
19
19
  "LICENSE"
20
20
  ],
21
- "scripts": {
22
- "build:client": "node scripts/build-client.mjs",
23
- "test": "node scripts/build-client.mjs --check && NODE_ENV=test node --test test/*.test.mjs"
24
- },
25
21
  "keywords": [
26
22
  "dsh",
27
23
  "dsh-plugin",
@@ -60,5 +56,9 @@
60
56
  },
61
57
  "devDependencies": {
62
58
  "@deepseek-ai/schemastery": "^3.18.4"
59
+ },
60
+ "scripts": {
61
+ "build:client": "node scripts/build-client.mjs",
62
+ "test": "node scripts/build-client.mjs --check && NODE_ENV=test node --test test/*.test.mjs"
63
63
  }
64
- }
64
+ }