@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 +8 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +52 -6
- package/lib/engine-milestones.js +6 -0
- package/lib/engine-verification.js +129 -0
- package/lib/goal-engine.js +11 -9
- package/lib/milestone-manager.js +48 -2
- package/lib/tools.js +13 -1
- package/package.json +6 -6
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
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
|
package/lib/engine-milestones.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -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
|
}
|
package/lib/milestone-manager.js
CHANGED
|
@@ -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, {
|
|
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.
|
|
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
|
+
}
|