@goodandready/dsh-goal 0.2.28 → 0.2.30
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/lib/client.js +370 -28
- package/lib/engine-operator-query.js +161 -0
- package/lib/engine-prompt.js +12 -0
- package/lib/engine-templates.js +15 -2
- package/lib/engine-verification.js +177 -7
- package/lib/goal-engine-constants.js +4 -0
- package/lib/goal-engine.js +41 -44
- package/lib/index.js +2 -0
- package/lib/milestone-manager.js +8 -3
- package/lib/routes.js +24 -0
- package/lib/tools.js +91 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -32,28 +32,152 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
|
|
33
33
|
// --- 10 QUICK LAUNCH ENGINEERING TEMPLATES ---
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{ id: 'feature', icon: '🚀', label: { en: 'New Feature', zh: '新功能开发' }, prefix: { en: 'Implement new feature according to specification and add tests for: ', zh: '根据规格说明实现新功能并补充测试:' } },
|
|
46
|
-
|
|
47
|
-
{ id: 'security', icon: '🛡️', label: { en: 'Security Audit', zh: '安全加固' }, prefix: { en: 'Audit security boundaries, sanitize inputs, and prevent injection in: ', zh: '审计安全边界,增加输入过滤与防注入保护:' } },
|
|
48
|
-
|
|
49
|
-
{ id: 'docs', icon: '📚', label: { en: 'Docs & Contract', zh: '文档规范' }, prefix: { en: 'Update technical documentation, README, and DESIGN.md conforming to standard for: ', zh: '按标准更新技术文档、README 与 DESIGN.md:' } },
|
|
50
|
-
|
|
51
|
-
{ id: 'upgrade', icon: '📦', label: { en: 'Upgrade Deps', zh: '依赖升级' }, prefix: { en: 'Upgrade dependencies, resolve breaking API changes, and verify build for: ', zh: '安全升级依赖库,解决接口兼容性并确保构建通过:' } },
|
|
52
|
-
|
|
53
|
-
{ id: 'cleanup', icon: '🧹', label: { en: 'Dead Code', zh: '清理死代码' }, prefix: { en: 'Find and safely remove dead code, unused exports, and stale fixtures in: ', zh: '查找并安全移除死代码、未使用的导出与失效测试用例:' } },
|
|
54
|
-
|
|
55
|
-
{ id: 'perf', icon: '⚙️', label: { en: 'Performance', zh: '性能优化' }, prefix: { en: 'Profile performance bottlenecks, eliminate unnecessary blocking calls in: ', zh: '分析性能瓶颈,消除阻塞调用并优化响应速度:' } },
|
|
35
|
+
function formatQuickLaunchPrompt(tmpl, vars, lang) {
|
|
36
|
+
if (!tmpl) return "";
|
|
37
|
+
const chosenLang = lang === "zh" ? "zh" : "en";
|
|
38
|
+
const tStr = tmpl.template ? (tmpl.template[chosenLang] || tmpl.template.en) : null;
|
|
39
|
+
if (!tStr) return tmpl.prefix ? (tmpl.prefix[chosenLang] || tmpl.prefix.en) : "";
|
|
40
|
+
return tStr.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_, key) => {
|
|
41
|
+
return (vars && vars[key] != null && vars[key] !== "") ? String(vars[key]) : "";
|
|
42
|
+
}).replace(/\s+/g, " ").trim();
|
|
43
|
+
}
|
|
56
44
|
|
|
45
|
+
const QUICK_LAUNCH_TEMPLATES = [
|
|
46
|
+
{
|
|
47
|
+
id: 'fix',
|
|
48
|
+
icon: "🐛",
|
|
49
|
+
label: { en: "Fix Bug", zh: "修复缺陷" },
|
|
50
|
+
prefix: { en: "Investigate bug, pinpoint root cause, apply fix, and verify with tests: ", zh: "调查并定位缺陷根源,实施修复并通过测试验证:" },
|
|
51
|
+
template: {
|
|
52
|
+
en: "Investigate bug {{issueNumber}} in {{targetModule}}, pinpoint root cause, apply targeted fix, and verify with tests: {{description}}",
|
|
53
|
+
zh: "调查缺陷 {{issueNumber}}(位于 {{targetModule}}),定位根因,实施修复并通过测试验证:{{description}}"
|
|
54
|
+
},
|
|
55
|
+
variables: [
|
|
56
|
+
{ id: "issueNumber", label: { en: "Issue #", zh: "Issue 编号" }, placeholder: '#issue-123', required: true },
|
|
57
|
+
{ id: "targetModule", label: { en: "Target Module", zh: "目标模块" }, placeholder: "lib/tools.js" },
|
|
58
|
+
{ id: 'description', label: { en: "Description / Symptom", zh: "症状或描述" }, placeholder: "Describe symptom..." },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'refactor',
|
|
63
|
+
icon: "⚡",
|
|
64
|
+
label: { en: "Refactor (YAGNI)", zh: "重构精简" },
|
|
65
|
+
prefix: { en: "Refactor module applying YAGNI principle, delete redundant abstractions, and verify: ", zh: "遵循 YAGNI 原则重构模块,删除冗余抽象并验证:" },
|
|
66
|
+
template: {
|
|
67
|
+
en: "Refactor module {{targetFile}} applying YAGNI principle to < {{maxLines}} lines, delete redundant abstractions, and verify tests.",
|
|
68
|
+
zh: "遵循 YAGNI 原则重构模块 {{targetFile}} 至 < {{maxLines}} 行,删除冗余代码并验证测试。"
|
|
69
|
+
},
|
|
70
|
+
variables: [
|
|
71
|
+
{ id: "targetFile", label: { en: "Target File", zh: "目标文件" }, placeholder: "lib/goal-engine.js", required: true },
|
|
72
|
+
{ id: "maxLines", label: { en: "Max Lines Threshold", zh: "行数上限" }, placeholder: "500" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'tests',
|
|
77
|
+
icon: "📝",
|
|
78
|
+
label: { en: "Tests & Coverage", zh: "补充测试" },
|
|
79
|
+
prefix: { en: "Write comprehensive unit tests covering edge cases for: ", zh: "为以下模块编写覆盖边界情况的完整单元测试:" },
|
|
80
|
+
template: {
|
|
81
|
+
en: "Write comprehensive unit tests in {{testFile}} covering edge cases for {{targetFile}}.",
|
|
82
|
+
zh: "在 {{testFile}} 中为 {{targetFile}} 编写覆盖边界情况的完整单元测试。"
|
|
83
|
+
},
|
|
84
|
+
variables: [
|
|
85
|
+
{ id: "targetFile", label: { en: "Module to Test", zh: "被测模块" }, placeholder: "lib/engine-verification.js", required: true },
|
|
86
|
+
{ id: "testFile", label: { en: "Test File Path", zh: "测试文件路径" }, placeholder: "test/acceptance-gate.test.mjs" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'review',
|
|
91
|
+
icon: "🔍",
|
|
92
|
+
label: { en: "Code Review", zh: "代码审计" },
|
|
93
|
+
prefix: { en: "Review codebase for subtle bugs, memory leaks, and over-engineering in: ", zh: "对代码库进行深入审查,排查潜在缺陷与过度设计:" },
|
|
94
|
+
template: {
|
|
95
|
+
en: "Review codebase for subtle bugs, memory leaks, and over-engineering in {{targetScope}}.",
|
|
96
|
+
zh: "对 {{targetScope}} 进行深入审查,排查潜在缺陷与过度设计。"
|
|
97
|
+
},
|
|
98
|
+
variables: [
|
|
99
|
+
{ id: "targetScope", label: { en: "Target Scope", zh: "审查范围" }, placeholder: "lib/ and src/client/", required: true },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'feature',
|
|
104
|
+
icon: "🚀",
|
|
105
|
+
label: { en: "New Feature", zh: "新功能开发" },
|
|
106
|
+
prefix: { en: "Implement new feature according to specification and add tests for: ", zh: "根据规格说明实现新功能并补充测试:" },
|
|
107
|
+
template: {
|
|
108
|
+
en: "Implement feature {{featureName}} according to specification {{specDetails}} and add comprehensive tests.",
|
|
109
|
+
zh: "根据规格 {{specDetails}} 实现新功能 {{featureName}} 并补充完整测试。"
|
|
110
|
+
},
|
|
111
|
+
variables: [
|
|
112
|
+
{ id: "featureName", label: { en: "Feature Name", zh: "功能名称" }, placeholder: "Bi-directional kanban sync", required: true },
|
|
113
|
+
{ id: "specDetails", label: { en: "Specification / Issue", zh: "规格或Issue" }, placeholder: '#issue-164' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'security',
|
|
118
|
+
icon: "🛡️",
|
|
119
|
+
label: { en: "Security Audit", zh: "安全加固" },
|
|
120
|
+
prefix: { en: "Audit security boundaries, sanitize inputs, and prevent injection in: ", zh: "审计安全边界,增加输入过滤与防注入保护:" },
|
|
121
|
+
template: {
|
|
122
|
+
en: "Audit security boundaries, sanitize inputs, and prevent injection in {{targetRoute}}.",
|
|
123
|
+
zh: "审计 {{targetRoute}} 的安全边界,增加输入过滤与防注入保护。"
|
|
124
|
+
},
|
|
125
|
+
variables: [
|
|
126
|
+
{ id: "targetRoute", label: { en: "Routes / Subsystem", zh: "路由或子系统" }, placeholder: "lib/routes.js", required: true },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'docs',
|
|
131
|
+
icon: "📚",
|
|
132
|
+
label: { en: "Docs & Contract", zh: "文档规范" },
|
|
133
|
+
prefix: { en: "Update technical documentation, README, and DESIGN.md conforming to standard for: ", zh: "按标准更新技术文档、README 与 DESIGN.md:" },
|
|
134
|
+
template: {
|
|
135
|
+
en: "Update technical documentation, README, and {{docTarget}} conforming to engineering standards.",
|
|
136
|
+
zh: "按标准更新技术文档、README 与 {{docTarget}}。"
|
|
137
|
+
},
|
|
138
|
+
variables: [
|
|
139
|
+
{ id: "docTarget", label: { en: "Doc Scope / Module", zh: "文档范围或模块" }, placeholder: "docs/design/DESIGN.md", required: true },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'upgrade',
|
|
144
|
+
icon: "📦",
|
|
145
|
+
label: { en: "Upgrade Deps", zh: "依赖升级" },
|
|
146
|
+
prefix: { en: "Upgrade dependencies, resolve breaking API changes, and verify build for: ", zh: "安全升级依赖库,解决接口兼容性并确保构建通过:" },
|
|
147
|
+
template: {
|
|
148
|
+
en: "Upgrade dependency {{depName}}, resolve breaking API changes, and verify build.",
|
|
149
|
+
zh: "升级依赖库 {{depName}},解决接口兼容性并确保构建通过。"
|
|
150
|
+
},
|
|
151
|
+
variables: [
|
|
152
|
+
{ id: "depName", label: { en: "Dependency Name", zh: "依赖包名" }, placeholder: "cordis@^4.0.0", required: true },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'cleanup',
|
|
157
|
+
icon: "🧹",
|
|
158
|
+
label: { en: "Dead Code", zh: "清理死代码" },
|
|
159
|
+
prefix: { en: "Find and safely remove dead code, unused exports, and stale fixtures in: ", zh: "查找并安全移除死代码、未使用的导出与失效测试用例:" },
|
|
160
|
+
template: {
|
|
161
|
+
en: "Find and safely remove dead code, unused exports, and stale fixtures in {{cleanupTarget}}.",
|
|
162
|
+
zh: "查找并安全移除 {{cleanupTarget}} 中的死代码、未使用的导出与失效测试用例。"
|
|
163
|
+
},
|
|
164
|
+
variables: [
|
|
165
|
+
{ id: "cleanupTarget", label: { en: "Target Scope", zh: "清理目标" }, placeholder: "src/client/ or unused fixtures", required: true },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'perf',
|
|
170
|
+
icon: "⚙️",
|
|
171
|
+
label: { en: "Performance", zh: "性能优化" },
|
|
172
|
+
prefix: { en: "Profile performance bottlenecks, eliminate unnecessary blocking calls in: ", zh: "分析性能瓶颈,消除阻塞调用并优化响应速度:" },
|
|
173
|
+
template: {
|
|
174
|
+
en: "Profile performance bottlenecks, eliminate unnecessary blocking calls in {{perfTarget}}.",
|
|
175
|
+
zh: "分析性能瓶颈,消除 {{perfTarget}} 中的阻塞调用并优化响应速度。"
|
|
176
|
+
},
|
|
177
|
+
variables: [
|
|
178
|
+
{ id: "perfTarget", label: { en: "Subsystem / Function", zh: "优化目标" }, placeholder: "buildClient or state flush", required: true },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
57
181
|
];
|
|
58
182
|
|
|
59
183
|
// --- SVG ICONS ---
|
|
@@ -1653,6 +1777,28 @@ window.__ModuleLoader__.load({
|
|
|
1653
1777
|
gap: 3px;
|
|
1654
1778
|
}
|
|
1655
1779
|
|
|
1780
|
+
.dsh-goal-template-chip.active {
|
|
1781
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
1782
|
+
font-weight: 600;
|
|
1783
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
.dsh-goal-operator-card {
|
|
1787
|
+
background: var(--dsw-alias-bg-sunken);
|
|
1788
|
+
border: 1px solid var(--dsw-alias-state-warning-primary);
|
|
1789
|
+
border-radius: 6px;
|
|
1790
|
+
padding: 10px 12px;
|
|
1791
|
+
margin-top: 8px;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
.dsh-goal-tpl-vars-box {
|
|
1795
|
+
background: var(--dsw-alias-bg-sunken);
|
|
1796
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
1797
|
+
border-radius: 6px;
|
|
1798
|
+
padding: 8px 10px;
|
|
1799
|
+
margin-bottom: 10px;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1656
1802
|
.dsh-goal-gate-error {
|
|
1657
1803
|
font-size: 11px;
|
|
1658
1804
|
color: var(--dsw-alias-state-danger-primary);
|
|
@@ -2736,6 +2882,8 @@ window.__ModuleLoader__.load({
|
|
|
2736
2882
|
|
|
2737
2883
|
const [title, setTitle] = useState('');
|
|
2738
2884
|
const [launchMode, setLaunchMode] = useState('act');
|
|
2885
|
+
const [activeQuickTpl, setActiveQuickTpl] = useState(null);
|
|
2886
|
+
const [quickTplVars, setQuickTplVars] = useState({});
|
|
2739
2887
|
const [showTemplates, setShowTemplates] = useState(false);
|
|
2740
2888
|
const [backendTemplates, setBackendTemplates] = useState([]);
|
|
2741
2889
|
const [selectedTpl, setSelectedTpl] = useState(null);
|
|
@@ -2925,16 +3073,27 @@ window.__ModuleLoader__.load({
|
|
|
2925
3073
|
|
|
2926
3074
|
type: 'button',
|
|
2927
3075
|
|
|
2928
|
-
className:
|
|
3076
|
+
className: `dsh-goal-template-chip ${activeQuickTpl?.id === tmpl.id ? 'active' : ''}`,
|
|
3077
|
+
style: activeQuickTpl?.id === tmpl.id ? { borderColor: 'var(--dsw-alias-brand-primary)', fontWeight: 600 } : {},
|
|
2929
3078
|
|
|
2930
3079
|
title: prefix,
|
|
2931
3080
|
|
|
2932
3081
|
onClick: () => {
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3082
|
+
const isSelected = activeQuickTpl?.id === tmpl.id;
|
|
3083
|
+
if (isSelected) {
|
|
3084
|
+
setActiveQuickTpl(null);
|
|
3085
|
+
setQuickTplVars({});
|
|
3086
|
+
} else {
|
|
3087
|
+
setActiveQuickTpl(tmpl);
|
|
3088
|
+
const initial = {};
|
|
3089
|
+
(tmpl.variables || []).forEach((v) => { initial[v.id] = ""; });
|
|
3090
|
+
setQuickTplVars(initial);
|
|
3091
|
+
const generated = typeof formatQuickLaunchPrompt === "function"
|
|
3092
|
+
? formatQuickLaunchPrompt(tmpl, initial, lang)
|
|
3093
|
+
: prefix;
|
|
3094
|
+
setTitle(generated || prefix);
|
|
3095
|
+
}
|
|
2936
3096
|
if (inputRef.current) inputRef.current.focus();
|
|
2937
|
-
|
|
2938
3097
|
},
|
|
2939
3098
|
|
|
2940
3099
|
},
|
|
@@ -2947,6 +3106,75 @@ window.__ModuleLoader__.load({
|
|
|
2947
3106
|
|
|
2948
3107
|
),
|
|
2949
3108
|
|
|
3109
|
+
activeQuickTpl?.variables && activeQuickTpl.variables.length > 0
|
|
3110
|
+
? React.createElement(
|
|
3111
|
+
'div',
|
|
3112
|
+
{
|
|
3113
|
+
className: 'dsh-goal-tpl-vars-box',
|
|
3114
|
+
style: {
|
|
3115
|
+
background: 'var(--dsw-alias-bg-sunken)',
|
|
3116
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
3117
|
+
borderRadius: 6,
|
|
3118
|
+
padding: '8px 10px',
|
|
3119
|
+
marginBottom: 10,
|
|
3120
|
+
},
|
|
3121
|
+
},
|
|
3122
|
+
React.createElement(
|
|
3123
|
+
'div',
|
|
3124
|
+
{
|
|
3125
|
+
style: {
|
|
3126
|
+
fontSize: 11,
|
|
3127
|
+
fontWeight: 600,
|
|
3128
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
3129
|
+
marginBottom: 6,
|
|
3130
|
+
display: 'flex',
|
|
3131
|
+
alignItems: 'center',
|
|
3132
|
+
justifyContent: 'space-between',
|
|
3133
|
+
},
|
|
3134
|
+
},
|
|
3135
|
+
React.createElement('span', null, `${activeQuickTpl.icon} ${activeQuickTpl.label[lang] || activeQuickTpl.label.en} ${t('quickTplVariablesTitle') || 'Parameters:'}`),
|
|
3136
|
+
React.createElement(
|
|
3137
|
+
'button',
|
|
3138
|
+
{
|
|
3139
|
+
type: 'button',
|
|
3140
|
+
className: 'dsh-goal-btn icon-only',
|
|
3141
|
+
style: { fontSize: 10, padding: '2px 4px', height: 'auto' },
|
|
3142
|
+
onClick: () => { setActiveQuickTpl(null); setQuickTplVars({}); },
|
|
3143
|
+
},
|
|
3144
|
+
'✕'
|
|
3145
|
+
)
|
|
3146
|
+
),
|
|
3147
|
+
activeQuickTpl.variables.map((v) => {
|
|
3148
|
+
const vLabel = v.label ? (v.label[lang] || v.label.en) : v.id;
|
|
3149
|
+
return React.createElement(
|
|
3150
|
+
'div',
|
|
3151
|
+
{ key: v.id, style: { marginBottom: 6 } },
|
|
3152
|
+
React.createElement(
|
|
3153
|
+
'label',
|
|
3154
|
+
{ style: { fontSize: 11, display: 'block', color: 'var(--dsw-alias-label-primary)', marginBottom: 2 } },
|
|
3155
|
+
vLabel,
|
|
3156
|
+
v.required ? React.createElement('span', { style: { color: 'var(--dsw-alias-state-danger-primary)', marginLeft: 3 } }, '*') : null
|
|
3157
|
+
),
|
|
3158
|
+
React.createElement('input', {
|
|
3159
|
+
type: 'text',
|
|
3160
|
+
className: 'dsh-goal-card-input',
|
|
3161
|
+
style: { width: '100%', boxSizing: 'border-box', fontSize: 12, padding: '4px 8px' },
|
|
3162
|
+
placeholder: v.placeholder || '',
|
|
3163
|
+
value: quickTplVars[v.id] || '',
|
|
3164
|
+
onChange: (e) => {
|
|
3165
|
+
const updated = { ...quickTplVars, [v.id]: e.target.value };
|
|
3166
|
+
setQuickTplVars(updated);
|
|
3167
|
+
const generated = typeof formatQuickLaunchPrompt === 'function'
|
|
3168
|
+
? formatQuickLaunchPrompt(activeQuickTpl, updated, lang)
|
|
3169
|
+
: activeQuickTpl.prefix[lang] || activeQuickTpl.prefix.en;
|
|
3170
|
+
setTitle(generated);
|
|
3171
|
+
},
|
|
3172
|
+
})
|
|
3173
|
+
);
|
|
3174
|
+
})
|
|
3175
|
+
)
|
|
3176
|
+
: null,
|
|
3177
|
+
|
|
2950
3178
|
React.createElement('input', {
|
|
2951
3179
|
|
|
2952
3180
|
ref: inputRef,
|
|
@@ -3177,6 +3405,7 @@ window.__ModuleLoader__.load({
|
|
|
3177
3405
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
3178
3406
|
|
|
3179
3407
|
const [isQuickLaunchOpen, setIsQuickLaunchOpen] = useState(false);
|
|
3408
|
+
const [operatorAnswerInput, setOperatorAnswerInput] = useState('');
|
|
3180
3409
|
|
|
3181
3410
|
const [isDockCollapsed, setIsDockCollapsed] = useState(() => {
|
|
3182
3411
|
try {
|
|
@@ -3701,6 +3930,7 @@ window.__ModuleLoader__.load({
|
|
|
3701
3930
|
const isCompleted = state.state === 'COMPLETED';
|
|
3702
3931
|
|
|
3703
3932
|
const isPlanMode = state.mode === 'plan' || state.state === 'PLANNING';
|
|
3933
|
+
const isWaitingOperator = Boolean(state.waitingForOperator && state.operatorQuery);
|
|
3704
3934
|
|
|
3705
3935
|
const formatElapsed = (totalSeconds) => {
|
|
3706
3936
|
const s = Math.max(0, Math.floor(Number(totalSeconds) || 0));
|
|
@@ -3756,8 +3986,8 @@ window.__ModuleLoader__.load({
|
|
|
3756
3986
|
),
|
|
3757
3987
|
React.createElement(
|
|
3758
3988
|
'span',
|
|
3759
|
-
{ className: `dsh-goal-badge ${isCompleted ? 'dsh-goal-badge-ok' : isPlanMode ? 'dsh-goal-badge-plan' : isPaused ? 'dsh-goal-badge-warn' : ''}` },
|
|
3760
|
-
isCompleted ? (t('goalCompleted') || 'Done') : isPlanMode ? (t('planMode') || 'Plan Mode') : isPaused ? (t('paused') || 'Paused') : (t('goalLabel') || 'Goal')
|
|
3989
|
+
{ className: `dsh-goal-badge ${isCompleted ? 'dsh-goal-badge-ok' : isWaitingOperator ? 'dsh-goal-badge-warn' : isPlanMode ? 'dsh-goal-badge-plan' : isPaused ? 'dsh-goal-badge-warn' : ''}` },
|
|
3990
|
+
isCompleted ? (t('goalCompleted') || 'Done') : isWaitingOperator ? ('❓ ' + (t('operatorInputRequired') || 'Input Needed')) : isPlanMode ? (t('planMode') || 'Plan Mode') : isPaused ? (t('paused') || 'Paused') : (t('goalLabel') || 'Goal')
|
|
3761
3991
|
),
|
|
3762
3992
|
React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
|
|
3763
3993
|
React.createElement('span', { className: 'dsh-goal-time' }, `⏱ ${liveElapsedStr}`),
|
|
@@ -3965,6 +4195,112 @@ window.__ModuleLoader__.load({
|
|
|
3965
4195
|
|
|
3966
4196
|
),
|
|
3967
4197
|
|
|
4198
|
+
isWaitingOperator
|
|
4199
|
+
? React.createElement(
|
|
4200
|
+
'div',
|
|
4201
|
+
{
|
|
4202
|
+
className: 'dsh-goal-operator-card',
|
|
4203
|
+
style: {
|
|
4204
|
+
background: 'var(--dsw-alias-bg-sunken)',
|
|
4205
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
4206
|
+
borderRadius: 6,
|
|
4207
|
+
padding: '10px 12px',
|
|
4208
|
+
marginTop: 8,
|
|
4209
|
+
},
|
|
4210
|
+
},
|
|
4211
|
+
React.createElement(
|
|
4212
|
+
'div',
|
|
4213
|
+
{
|
|
4214
|
+
style: {
|
|
4215
|
+
display: 'flex',
|
|
4216
|
+
alignItems: 'center',
|
|
4217
|
+
gap: 6,
|
|
4218
|
+
fontSize: 12,
|
|
4219
|
+
fontWeight: 600,
|
|
4220
|
+
color: 'var(--dsw-alias-state-warning-primary)',
|
|
4221
|
+
marginBottom: 4,
|
|
4222
|
+
},
|
|
4223
|
+
},
|
|
4224
|
+
React.createElement('span', null, '❓'),
|
|
4225
|
+
React.createElement('span', null, t('operatorInputRequired') || 'Operator Input Required')
|
|
4226
|
+
),
|
|
4227
|
+
React.createElement(
|
|
4228
|
+
'div',
|
|
4229
|
+
{
|
|
4230
|
+
style: {
|
|
4231
|
+
fontSize: 13,
|
|
4232
|
+
fontWeight: 500,
|
|
4233
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
4234
|
+
marginBottom: 8,
|
|
4235
|
+
wordBreak: 'break-word',
|
|
4236
|
+
},
|
|
4237
|
+
},
|
|
4238
|
+
state.operatorQuery.question
|
|
4239
|
+
),
|
|
4240
|
+
Array.isArray(state.operatorQuery.options) && state.operatorQuery.options.length > 0
|
|
4241
|
+
? React.createElement(
|
|
4242
|
+
'div',
|
|
4243
|
+
{
|
|
4244
|
+
className: 'dsh-goal-operator-options',
|
|
4245
|
+
style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 },
|
|
4246
|
+
},
|
|
4247
|
+
state.operatorQuery.options.map((opt, idx) =>
|
|
4248
|
+
React.createElement(
|
|
4249
|
+
'button',
|
|
4250
|
+
{
|
|
4251
|
+
key: idx,
|
|
4252
|
+
type: 'button',
|
|
4253
|
+
className: 'dsh-goal-btn',
|
|
4254
|
+
style: {
|
|
4255
|
+
fontSize: 12,
|
|
4256
|
+
padding: '4px 10px',
|
|
4257
|
+
borderColor: 'var(--dsw-alias-brand-primary)',
|
|
4258
|
+
color: 'var(--dsw-alias-brand-primary)',
|
|
4259
|
+
},
|
|
4260
|
+
onClick: () => handleAction('answer_operator', { answer: opt, optionIndex: idx }),
|
|
4261
|
+
},
|
|
4262
|
+
opt
|
|
4263
|
+
)
|
|
4264
|
+
)
|
|
4265
|
+
)
|
|
4266
|
+
: null,
|
|
4267
|
+
React.createElement(
|
|
4268
|
+
'div',
|
|
4269
|
+
{ style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
4270
|
+
React.createElement('input', {
|
|
4271
|
+
type: 'text',
|
|
4272
|
+
className: 'dsh-goal-card-input',
|
|
4273
|
+
style: { flex: 1, fontSize: 12, padding: '4px 8px' },
|
|
4274
|
+
placeholder: t('answerPlaceholder') || 'Type your answer or select an option above...',
|
|
4275
|
+
value: operatorAnswerInput,
|
|
4276
|
+
onChange: (e) => setOperatorAnswerInput(e.target.value),
|
|
4277
|
+
onKeyDown: (e) => {
|
|
4278
|
+
if (e.key === 'Enter' && operatorAnswerInput.trim()) {
|
|
4279
|
+
e.preventDefault();
|
|
4280
|
+
handleAction('answer_operator', { answer: operatorAnswerInput.trim() });
|
|
4281
|
+
setOperatorAnswerInput('');
|
|
4282
|
+
}
|
|
4283
|
+
},
|
|
4284
|
+
}),
|
|
4285
|
+
React.createElement(
|
|
4286
|
+
'button',
|
|
4287
|
+
{
|
|
4288
|
+
type: 'button',
|
|
4289
|
+
className: 'dsh-goal-btn dsh-goal-btn-primary',
|
|
4290
|
+
style: { fontSize: 12, padding: '4px 12px', whiteSpace: 'nowrap' },
|
|
4291
|
+
disabled: !operatorAnswerInput.trim(),
|
|
4292
|
+
onClick: () => {
|
|
4293
|
+
if (operatorAnswerInput.trim()) {
|
|
4294
|
+
handleAction('answer_operator', { answer: operatorAnswerInput.trim() });
|
|
4295
|
+
setOperatorAnswerInput('');
|
|
4296
|
+
}
|
|
4297
|
+
},
|
|
4298
|
+
},
|
|
4299
|
+
t('submitAnswer') || 'Submit'
|
|
4300
|
+
)
|
|
4301
|
+
)
|
|
4302
|
+
)
|
|
4303
|
+
: null
|
|
3968
4304
|
),
|
|
3969
4305
|
|
|
3970
4306
|
),
|
|
@@ -5081,6 +5417,12 @@ window.__ModuleLoader__.load({
|
|
|
5081
5417
|
mergeBranch: 'Merge Branch',
|
|
5082
5418
|
discardBranch: 'Discard Branch',
|
|
5083
5419
|
retrospectiveTitle: 'Goal Retrospective',
|
|
5420
|
+
operatorInputRequired: 'Operator Input Required',
|
|
5421
|
+
operatorQuestion: 'Question from agent:',
|
|
5422
|
+
submitAnswer: 'Submit',
|
|
5423
|
+
answerPlaceholder: 'Type your answer or select an option above...',
|
|
5424
|
+
quickTplVariablesTitle: 'Parameters:',
|
|
5425
|
+
requiredField: 'Required',
|
|
5084
5426
|
verificationGate: 'Acceptance gate',
|
|
5085
5427
|
verificationPassed: 'Verified',
|
|
5086
5428
|
verificationFailed: 'Gate failed',
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { GoalState } from './goal-engine-constants.js';
|
|
2
|
+
import { recordActivity } from './engine-intervention.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ask the human operator a question when encountering ambiguity or critical decisions (#166).
|
|
6
|
+
* Sets the goal into a waiting/paused state and records the query.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} goal
|
|
9
|
+
* @param {object} query
|
|
10
|
+
* @param {string} query.question - Question text
|
|
11
|
+
* @param {string[]} [query.options] - Predefined choices for the operator
|
|
12
|
+
* @param {boolean} [query.isBlocking=true] - Whether to pause execution until answered
|
|
13
|
+
* @returns {object} result
|
|
14
|
+
*/
|
|
15
|
+
export function askOperatorQuery(goal, query = {}) {
|
|
16
|
+
if (!goal) {
|
|
17
|
+
return { ok: false, error: 'No active goal' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rawQ = query.question ? String(query.question).trim() : '';
|
|
21
|
+
if (!rawQ) {
|
|
22
|
+
return { ok: false, error: 'Question text is required' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cleanOptions = Array.isArray(query.options)
|
|
26
|
+
? query.options.map((o) => String(o).trim()).filter(Boolean)
|
|
27
|
+
: [];
|
|
28
|
+
|
|
29
|
+
const isBlocking = query.isBlocking !== false;
|
|
30
|
+
|
|
31
|
+
const queryId = `opq-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
32
|
+
goal.operatorQuery = {
|
|
33
|
+
id: queryId,
|
|
34
|
+
question: rawQ,
|
|
35
|
+
options: cleanOptions,
|
|
36
|
+
isBlocking,
|
|
37
|
+
askedAt: Date.now(),
|
|
38
|
+
answeredAt: null,
|
|
39
|
+
answer: null,
|
|
40
|
+
optionIndex: null,
|
|
41
|
+
consumed: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
goal.waitingForOperator = true;
|
|
45
|
+
|
|
46
|
+
if (isBlocking) {
|
|
47
|
+
if (!goal.previousStateBeforeOperator) {
|
|
48
|
+
goal.previousStateBeforeOperator = goal.state || GoalState.RUNNING;
|
|
49
|
+
}
|
|
50
|
+
goal.state = GoalState.PAUSED;
|
|
51
|
+
if (!goal.pausedAt) {
|
|
52
|
+
goal.pausedAt = Date.now();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
recordActivity(goal, {
|
|
57
|
+
type: 'operator_query',
|
|
58
|
+
tool: 'goal_ask_operator',
|
|
59
|
+
status: 'pending',
|
|
60
|
+
details: `Asked operator: "${rawQ.slice(0, 80)}"`,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!Array.isArray(goal.logs)) goal.logs = [];
|
|
64
|
+
goal.logs.push({
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
type: 'warning',
|
|
67
|
+
message: `Goal awaiting operator input: "${rawQ}"`,
|
|
68
|
+
});
|
|
69
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
70
|
+
|
|
71
|
+
return { ok: true, queryId, question: rawQ, options: cleanOptions, isBlocking };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Record operator response to a pending query and resume goal execution (#166).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} goal
|
|
78
|
+
* @param {object} payload
|
|
79
|
+
* @param {string} [payload.answer] - Freeform text answer
|
|
80
|
+
* @param {number} [payload.optionIndex] - Index of selected choice in options array
|
|
81
|
+
* @returns {object} result
|
|
82
|
+
*/
|
|
83
|
+
export function answerOperatorQuery(goal, payload = {}) {
|
|
84
|
+
if (!goal || !goal.operatorQuery || !goal.waitingForOperator) {
|
|
85
|
+
return { ok: false, error: 'No pending operator query for this goal' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let finalAnswer = typeof payload.answer === 'string' ? payload.answer.trim() : '';
|
|
89
|
+
const optIdx = typeof payload.optionIndex === 'number' ? payload.optionIndex : null;
|
|
90
|
+
|
|
91
|
+
if (optIdx !== null && Array.isArray(goal.operatorQuery.options) && goal.operatorQuery.options[optIdx]) {
|
|
92
|
+
finalAnswer = goal.operatorQuery.options[optIdx];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!finalAnswer) {
|
|
96
|
+
return { ok: false, error: 'Answer or valid option selection is required' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
goal.operatorQuery.answeredAt = Date.now();
|
|
100
|
+
goal.operatorQuery.answer = finalAnswer;
|
|
101
|
+
goal.operatorQuery.optionIndex = optIdx;
|
|
102
|
+
goal.operatorQuery.consumed = false;
|
|
103
|
+
goal.waitingForOperator = false;
|
|
104
|
+
|
|
105
|
+
// Unpause if goal was paused for operator
|
|
106
|
+
if (goal.pausedAt) {
|
|
107
|
+
const pauseMs = Math.max(0, Date.now() - goal.pausedAt);
|
|
108
|
+
goal.totalPausedDurationMs = (goal.totalPausedDurationMs || 0) + pauseMs;
|
|
109
|
+
goal.pausedAt = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
goal.state = goal.previousStateBeforeOperator || GoalState.RUNNING;
|
|
113
|
+
delete goal.previousStateBeforeOperator;
|
|
114
|
+
|
|
115
|
+
recordActivity(goal, {
|
|
116
|
+
type: 'operator_answer',
|
|
117
|
+
tool: 'goal_ask_operator',
|
|
118
|
+
status: 'success',
|
|
119
|
+
details: `Operator replied: "${finalAnswer.slice(0, 80)}"`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!Array.isArray(goal.logs)) goal.logs = [];
|
|
123
|
+
goal.logs.push({
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
type: 'info',
|
|
126
|
+
message: `Operator answered: "${finalAnswer}"`,
|
|
127
|
+
});
|
|
128
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
129
|
+
|
|
130
|
+
return { ok: true, answer: finalAnswer };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format prompt injection with operator's answer for the next turn.
|
|
135
|
+
* Marks the answer as consumed after formatting.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} goal
|
|
138
|
+
* @param {string} [lang='en']
|
|
139
|
+
* @returns {string} formatted prompt snippet or empty string
|
|
140
|
+
*/
|
|
141
|
+
export function formatOperatorAnswerPrompt(goal, lang = 'en') {
|
|
142
|
+
if (!goal || !goal.operatorQuery || !goal.operatorQuery.answer || goal.operatorQuery.consumed) {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { question, answer } = goal.operatorQuery;
|
|
147
|
+
// Mark consumed so it is only injected on the resume turn
|
|
148
|
+
goal.operatorQuery.consumed = true;
|
|
149
|
+
|
|
150
|
+
if (lang === 'zh') {
|
|
151
|
+
return `\n\n[操作员答复 / OPERATOR RESPONSE]\n` +
|
|
152
|
+
`你之前提出的问题: "${question}"\n` +
|
|
153
|
+
`操作员给出的明确指令: "${answer}"\n` +
|
|
154
|
+
`请严格根据操作员的答复继续推进任务,切勿再对此问题做无谓猜测!\n`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return `\n\n[OPERATOR RESPONSE TO YOUR QUERY]\n` +
|
|
158
|
+
`Your Question: "${question}"\n` +
|
|
159
|
+
`Operator Answer: "${answer}"\n` +
|
|
160
|
+
`Strictly follow the operator's response and proceed with executing the goal without second-guessing!\n`;
|
|
161
|
+
}
|
package/lib/engine-prompt.js
CHANGED
|
@@ -9,6 +9,16 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
9
9
|
const hasMilestones = snapshot.milestones && snapshot.milestones.length > 0;
|
|
10
10
|
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
11
11
|
|
|
12
|
+
let operatorAnswerText = '';
|
|
13
|
+
if (snapshot.operatorQuery?.answer && !snapshot.operatorQuery.consumed) {
|
|
14
|
+
snapshot.operatorQuery.consumed = true;
|
|
15
|
+
if (lang === 'zh') {
|
|
16
|
+
operatorAnswerText = `\n\n[操作员答复 / OPERATOR RESPONSE]\n你之前提出的问题: "${snapshot.operatorQuery.question}"\n操作员给出的明确指令: "${snapshot.operatorQuery.answer}"\n请严格根据操作员的答复继续推进任务,切勿再对此问题做无谓猜测!\n`;
|
|
17
|
+
} else {
|
|
18
|
+
operatorAnswerText = `\n\n[OPERATOR RESPONSE TO YOUR QUERY]\nYour Question: "${snapshot.operatorQuery.question}"\nOperator Answer: "${snapshot.operatorQuery.answer}"\nStrictly follow the operator's response and proceed with executing the goal without second-guessing!\n`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
let directiveText = '';
|
|
13
23
|
if (snapshot.userDirective && !snapshot.userDirectiveConsumed) {
|
|
14
24
|
if (lang === 'zh') {
|
|
@@ -67,6 +77,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
67
77
|
: ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
|
|
68
78
|
|
|
69
79
|
return (
|
|
80
|
+
operatorAnswerText +
|
|
70
81
|
directiveText +
|
|
71
82
|
nudgeText +
|
|
72
83
|
budgetWarningText +
|
|
@@ -87,6 +98,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
87
98
|
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
88
99
|
|
|
89
100
|
return (
|
|
101
|
+
operatorAnswerText +
|
|
90
102
|
directiveText +
|
|
91
103
|
nudgeText +
|
|
92
104
|
budgetWarningText +
|
package/lib/engine-templates.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format template string by replacing {{key}} with provided variable value (#165)
|
|
3
|
+
* @param {string} templateStr
|
|
4
|
+
* @param {object} vars
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function formatTemplateVariables(templateStr, vars = {}) {
|
|
8
|
+
if (!templateStr || typeof templateStr !== "string") return "";
|
|
9
|
+
return templateStr.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_, key) => {
|
|
10
|
+
return vars && vars[key] != null && vars[key] !== "" ? String(vars[key]) : "";
|
|
11
|
+
}).trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
import { MilestoneStatus } from './goal-engine-constants.js';
|
|
2
15
|
|
|
3
16
|
/**
|
|
@@ -93,8 +106,8 @@ export function instantiateTemplate(templateId, inputValues = {}) {
|
|
|
93
106
|
const tpl = GOAL_TEMPLATES.find((t) => t.id === templateId);
|
|
94
107
|
if (!tpl) return null;
|
|
95
108
|
|
|
96
|
-
let title = tpl.title;
|
|
97
|
-
let description = tpl.description;
|
|
109
|
+
let title = tpl.promptTemplate ? formatTemplateVariables(tpl.promptTemplate, inputValues) : tpl.title;
|
|
110
|
+
let description = tpl.descriptionTemplate ? formatTemplateVariables(tpl.descriptionTemplate, inputValues) : tpl.description;
|
|
98
111
|
if (inputValues.targetFile) {
|
|
99
112
|
title = `Refactor ${inputValues.targetFile}`;
|
|
100
113
|
description = `Decompose ${inputValues.targetFile} to < ${inputValues.lineThreshold || 500} lines`;
|
|
@@ -1,39 +1,209 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { getSafeCwd } from './goal-engine-constants.js';
|
|
3
3
|
|
|
4
|
+
export const SAFE_ENV_WHITELIST = [
|
|
5
|
+
'PATH',
|
|
6
|
+
'HOME',
|
|
7
|
+
'USER',
|
|
8
|
+
'LOGNAME',
|
|
9
|
+
'LANG',
|
|
10
|
+
'LC_ALL',
|
|
11
|
+
'LC_CTYPE',
|
|
12
|
+
'SHELL',
|
|
13
|
+
'TERM',
|
|
14
|
+
'TMPDIR',
|
|
15
|
+
'TMP',
|
|
16
|
+
'TEMP',
|
|
17
|
+
'NODE_ENV',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const FORBIDDEN_SECRET_REGEX = /(TOKEN|KEY|SECRET|AUTH|PASS|PASSWORD|CREDENTIAL|PRIVATE|BEARER|API)/i;
|
|
21
|
+
|
|
22
|
+
export const DISALLOWED_BINARIES = new Set([
|
|
23
|
+
'curl', 'wget', 'nc', 'ncat', 'netcat', 'telnet',
|
|
24
|
+
'ssh', 'scp', 'sftp', 'ftp', 'rsync',
|
|
25
|
+
'rm', 'dd', 'mkfs', 'shutdown', 'reboot', 'killall', 'kill',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const SHELL_OPERATORS = new Set([';', '&&', '||', '|', '&', '>', '>>', '<', '`']);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sanitize environment for child verification processes, strictly stripping tokens and secrets
|
|
32
|
+
* @param {object} [userEnv={}]
|
|
33
|
+
* @returns {object}
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeVerificationEnv(userEnv = {}) {
|
|
36
|
+
const clean = {};
|
|
37
|
+
for (const key of SAFE_ENV_WHITELIST) {
|
|
38
|
+
if (process.env[key] !== undefined && !FORBIDDEN_SECRET_REGEX.test(key)) {
|
|
39
|
+
clean[key] = process.env[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (userEnv && typeof userEnv === 'object') {
|
|
43
|
+
for (const [k, v] of Object.entries(userEnv)) {
|
|
44
|
+
if (k && !FORBIDDEN_SECRET_REGEX.test(k) && v !== undefined && v !== null) {
|
|
45
|
+
clean[k] = String(v);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!clean.NODE_ENV) {
|
|
50
|
+
clean.NODE_ENV = 'test';
|
|
51
|
+
}
|
|
52
|
+
return clean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse a command string or array into an argv list
|
|
57
|
+
* @param {string|string[]} commandStr
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
export function parseCommandArgv(commandStr) {
|
|
61
|
+
if (Array.isArray(commandStr)) {
|
|
62
|
+
return commandStr.map(String).filter((s) => s.trim().length > 0);
|
|
63
|
+
}
|
|
64
|
+
if (!commandStr || typeof commandStr !== 'string') return [];
|
|
65
|
+
const trimmed = commandStr.trim();
|
|
66
|
+
if (!trimmed) return [];
|
|
67
|
+
|
|
68
|
+
const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g;
|
|
69
|
+
const args = [];
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = regex.exec(trimmed)) !== null) {
|
|
72
|
+
if (match[1] !== undefined) {
|
|
73
|
+
args.push(match[1].replace(/\\"/g, '"'));
|
|
74
|
+
} else if (match[2] !== undefined) {
|
|
75
|
+
args.push(match[2].replace(/\\'/g, "'"));
|
|
76
|
+
} else if (match[3] !== undefined) {
|
|
77
|
+
args.push(match[3]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return args;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate a verification command for dangerous operators, shell chaining, or exfiltration binaries
|
|
85
|
+
* @param {string|string[]} command
|
|
86
|
+
* @returns {{ valid: boolean, error?: string, argv?: string[], binary?: string }}
|
|
87
|
+
*/
|
|
88
|
+
export function validateVerificationCommand(command) {
|
|
89
|
+
if (!command) return { valid: false, error: 'Empty or invalid verification command' };
|
|
90
|
+
const argv = parseCommandArgv(command);
|
|
91
|
+
if (argv.length === 0) return { valid: false, error: 'Empty or invalid verification command' };
|
|
92
|
+
|
|
93
|
+
const rawBinary = argv[0].toLowerCase().replace(/^.*[/\\]/, '');
|
|
94
|
+
if (DISALLOWED_BINARIES.has(rawBinary)) {
|
|
95
|
+
return { valid: false, error: `Execution blocked: disallowed command binary "${rawBinary}"` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const arg = argv[i];
|
|
100
|
+
if (SHELL_OPERATORS.has(arg)) {
|
|
101
|
+
return { valid: false, error: `Execution blocked: shell operator "${arg}" is not allowed` };
|
|
102
|
+
}
|
|
103
|
+
if (arg.startsWith('$(') || (arg.startsWith('`') && arg.endsWith('`'))) {
|
|
104
|
+
return { valid: false, error: 'Execution blocked: subshell substitution is not allowed' };
|
|
105
|
+
}
|
|
106
|
+
if (i > 0 && DISALLOWED_BINARIES.has(arg.toLowerCase().replace(/^.*[/\\]/, ''))) {
|
|
107
|
+
return { valid: false, error: `Execution blocked: disallowed binary "${arg}" in arguments` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { valid: true, argv, binary: rawBinary };
|
|
112
|
+
}
|
|
113
|
+
|
|
4
114
|
/**
|
|
5
115
|
* Execute a verification command in a confined working directory with timeout and output capture
|
|
6
|
-
*
|
|
116
|
+
* Uses sanitized environment (no secrets/tokens) and shell: false (no shell injection)
|
|
117
|
+
* @param {string|string[]} command Command to execute
|
|
7
118
|
* @param {string} [targetCwd] Working directory
|
|
8
119
|
* @param {object} [options]
|
|
9
120
|
* @param {number} [options.timeoutMs=30000] Timeout in milliseconds
|
|
10
121
|
* @param {number} [options.maxBuffer=1048576] Max stdout/stderr buffer in bytes (1MB)
|
|
11
|
-
* @param {object} [options.env={}] Additional environment variables
|
|
122
|
+
* @param {object} [options.env={}] Additional environment variables (sanitized)
|
|
123
|
+
* @param {Function} [options.checkApproval] Custom approval check function
|
|
124
|
+
* @param {object} [options.approvalGate] ApprovalGate service instance
|
|
12
125
|
* @returns {{ ok: boolean, exitCode: number, stdout: string, stderr: string, error?: string }}
|
|
13
126
|
*/
|
|
14
127
|
export function runMilestoneVerification(command, targetCwd, options = {}) {
|
|
15
|
-
|
|
128
|
+
const validation = validateVerificationCommand(command);
|
|
129
|
+
if (!validation.valid) {
|
|
16
130
|
return {
|
|
17
131
|
ok: false,
|
|
18
132
|
exitCode: 1,
|
|
19
133
|
stdout: '',
|
|
20
134
|
stderr: '',
|
|
21
|
-
error: 'Empty or invalid verification command',
|
|
135
|
+
error: validation.error || 'Empty or invalid verification command',
|
|
22
136
|
};
|
|
23
137
|
}
|
|
24
138
|
|
|
25
139
|
const safeCwd = getSafeCwd(targetCwd) || process.cwd();
|
|
26
140
|
const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 30000;
|
|
27
141
|
const maxBuffer = typeof options.maxBuffer === 'number' ? options.maxBuffer : 1024 * 1024;
|
|
142
|
+
const { argv, binary } = validation;
|
|
143
|
+
|
|
144
|
+
// 1. Approval seam check
|
|
145
|
+
if (typeof options.checkApproval === 'function') {
|
|
146
|
+
const decision = options.checkApproval({
|
|
147
|
+
command,
|
|
148
|
+
argv,
|
|
149
|
+
binary,
|
|
150
|
+
cwd: safeCwd,
|
|
151
|
+
});
|
|
152
|
+
if (decision && (decision.kind === 'deny' || decision.deny)) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
exitCode: 1,
|
|
156
|
+
stdout: '',
|
|
157
|
+
stderr: '',
|
|
158
|
+
error: `Verification command denied by policy: ${decision.reason || 'denied'}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (decision && (decision.kind === 'ask' || decision.ask)) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
exitCode: 1,
|
|
165
|
+
stdout: '',
|
|
166
|
+
stderr: '',
|
|
167
|
+
error: `Verification command requires operator approval: ${decision.reason || 'approval required'}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (options.approvalGate && typeof options.approvalGate.checkCommand === 'function') {
|
|
173
|
+
try {
|
|
174
|
+
const decision = options.approvalGate.checkCommand(command, safeCwd);
|
|
175
|
+
if (decision && (decision.verdict === 'deny' || decision.verdict === 'ask')) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
exitCode: 1,
|
|
179
|
+
stdout: '',
|
|
180
|
+
stderr: '',
|
|
181
|
+
error: `Verification command blocked by approval gate: ${decision.reason || decision.verdict}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
exitCode: 1,
|
|
188
|
+
stdout: '',
|
|
189
|
+
stderr: '',
|
|
190
|
+
error: `Approval check error: ${err?.message || err}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 2. Environment sanitization (strictly scrub secrets and tokens)
|
|
196
|
+
const cleanEnv = sanitizeVerificationEnv(options.env);
|
|
28
197
|
|
|
198
|
+
// 3. Execution without shell (shell: false)
|
|
29
199
|
try {
|
|
30
|
-
const res = spawnSync(
|
|
31
|
-
shell:
|
|
200
|
+
const res = spawnSync(argv[0], argv.slice(1), {
|
|
201
|
+
shell: false,
|
|
32
202
|
cwd: safeCwd,
|
|
33
203
|
timeout: timeoutMs,
|
|
34
204
|
maxBuffer,
|
|
35
205
|
encoding: 'utf8',
|
|
36
|
-
env:
|
|
206
|
+
env: cleanEnv,
|
|
37
207
|
});
|
|
38
208
|
|
|
39
209
|
if (res.error) {
|
|
@@ -192,6 +192,8 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
192
192
|
enableMilestoneDependencies: engine.enableMilestoneDependencies ?? true,
|
|
193
193
|
soundScheme: engine.soundScheme ?? 'default',
|
|
194
194
|
enableVoiceAnnouncements: engine.enableVoiceAnnouncements ?? false,
|
|
195
|
+
operatorQuery: null,
|
|
196
|
+
waitingForOperator: false,
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
|
|
@@ -251,6 +253,8 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
251
253
|
enableMilestoneDependencies: engine.enableMilestoneDependencies ?? true,
|
|
252
254
|
soundScheme: engine.soundScheme ?? 'default',
|
|
253
255
|
enableVoiceAnnouncements: engine.enableVoiceAnnouncements ?? false,
|
|
256
|
+
operatorQuery: goal.operatorQuery || null,
|
|
257
|
+
waitingForOperator: Boolean(goal.waitingForOperator),
|
|
254
258
|
};
|
|
255
259
|
}
|
|
256
260
|
|
package/lib/goal-engine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { verifyMilestoneAcceptance } from './engine-verification.js';
|
|
2
|
+
import { askOperatorQuery, answerOperatorQuery } from './engine-operator-query.js';
|
|
2
3
|
import { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot, getSafeCwd } from './goal-engine-constants.js';
|
|
3
4
|
import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
|
|
4
5
|
import { EngineStore } from './engine-store.js';
|
|
@@ -11,8 +12,7 @@ import { generateRetrospectiveData } from './engine-retrospective.js';
|
|
|
11
12
|
import { generatePreplanDraft } from './engine-templates.js';
|
|
12
13
|
import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget, setGoalMode, pauseGoal, resumeGoal, cancelGoal } from './engine-sessions.js';
|
|
13
14
|
|
|
14
|
-
export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist };
|
|
15
|
-
export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
15
|
+
export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Isolated Goal Management Engine for DeepSeek Harness
|
|
@@ -22,23 +22,19 @@ export class GoalEngine {
|
|
|
22
22
|
this.logger = options.logger || console;
|
|
23
23
|
const rawDefIters = options.defaultMaxIterations ?? 25;
|
|
24
24
|
this.defaultMaxIterations = typeof rawDefIters === "number" ? rawDefIters : (typeof rawDefIters?.value === "number" ? rawDefIters.value : 25);
|
|
25
|
-
this
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.enableMilestoneDependencies = options.enableMilestoneDependencies ?? true;
|
|
39
|
-
this.soundScheme = options.soundScheme ?? 'default';
|
|
40
|
-
this.enableVoiceAnnouncements = options.enableVoiceAnnouncements ?? false;
|
|
41
|
-
this.maxSessions = options.maxSessions ?? 100;
|
|
25
|
+
Object.assign(this, {
|
|
26
|
+
autoDrive: options.autoDrive ?? true, enableSound: options.enableSound ?? true,
|
|
27
|
+
showQuickLaunchButton: options.showQuickLaunchButton ?? true,
|
|
28
|
+
consecutiveToolFailureLimit: options.consecutiveToolFailureLimit ?? 3,
|
|
29
|
+
maxTokenBudget: options.maxTokenBudget ?? 0, budgetWarningThreshold: options.budgetWarningThreshold ?? 80,
|
|
30
|
+
autoCheckpointOnMilestone: options.autoCheckpointOnMilestone ?? false,
|
|
31
|
+
enableLiveActivityFeed: options.enableLiveActivityFeed ?? true, enablePreplanning: options.enablePreplanning ?? true,
|
|
32
|
+
autoBranchOnGoalStart: options.autoBranchOnGoalStart ?? true, enablePostGoalRetrospective: options.enablePostGoalRetrospective ?? true,
|
|
33
|
+
autoScaleBudgetNearCompletion: options.autoScaleBudgetNearCompletion ?? false, enableTemplatesDrawer: options.enableTemplatesDrawer ?? true,
|
|
34
|
+
enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default',
|
|
35
|
+
enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, approvalGate: options.approvalGate || null,
|
|
36
|
+
checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100,
|
|
37
|
+
});
|
|
42
38
|
this.goals = new Map();
|
|
43
39
|
this.listeners = new Set();
|
|
44
40
|
this.stallCounters = new Map();
|
|
@@ -52,10 +48,7 @@ export class GoalEngine {
|
|
|
52
48
|
set storagePath(val) { this.store.setStoragePath(val); }
|
|
53
49
|
get saveTimer() { return this.store.saveTimer; }
|
|
54
50
|
get currentGoal() { return this.goals.get('default') || null; }
|
|
55
|
-
set currentGoal(val) {
|
|
56
|
-
if (val) this.goals.set('default', val);
|
|
57
|
-
else this.goals.delete('default');
|
|
58
|
-
}
|
|
51
|
+
set currentGoal(val) { if (val) this.goals.set('default', val); else this.goals.delete('default'); }
|
|
59
52
|
|
|
60
53
|
loadStateFromDisk() { this.store.loadStateFromDisk(); }
|
|
61
54
|
scheduleSave(immediate = false) { this.store.scheduleSave(immediate); }
|
|
@@ -109,22 +102,12 @@ export class GoalEngine {
|
|
|
109
102
|
}
|
|
110
103
|
}
|
|
111
104
|
}
|
|
112
|
-
|
|
113
|
-
if (typeof config
|
|
114
|
-
if (typeof config.showQuickLaunchButton === 'boolean') this.showQuickLaunchButton = config.showQuickLaunchButton;
|
|
105
|
+
const boolKeys = ['autoDrive', 'enableSound', 'showQuickLaunchButton', 'autoCheckpointOnMilestone', 'enableLiveActivityFeed', 'enablePreplanning', 'autoBranchOnGoalStart', 'enablePostGoalRetrospective', 'autoScaleBudgetNearCompletion', 'enableTemplatesDrawer', 'enableMilestoneDependencies', 'enableVoiceAnnouncements'];
|
|
106
|
+
for (const k of boolKeys) if (typeof config[k] === 'boolean') this[k] = config[k];
|
|
115
107
|
if (typeof config.consecutiveToolFailureLimit === 'number') this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
|
|
116
108
|
if (typeof config.maxTokenBudget === 'number') this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
|
|
117
109
|
if (typeof config.budgetWarningThreshold === 'number') this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
|
|
118
|
-
if (typeof config.autoCheckpointOnMilestone === 'boolean') this.autoCheckpointOnMilestone = config.autoCheckpointOnMilestone;
|
|
119
|
-
if (typeof config.enableLiveActivityFeed === 'boolean') this.enableLiveActivityFeed = config.enableLiveActivityFeed;
|
|
120
|
-
if (typeof config.enablePreplanning === 'boolean') this.enablePreplanning = config.enablePreplanning;
|
|
121
|
-
if (typeof config.autoBranchOnGoalStart === 'boolean') this.autoBranchOnGoalStart = config.autoBranchOnGoalStart;
|
|
122
|
-
if (typeof config.enablePostGoalRetrospective === 'boolean') this.enablePostGoalRetrospective = config.enablePostGoalRetrospective;
|
|
123
|
-
if (typeof config.autoScaleBudgetNearCompletion === 'boolean') this.autoScaleBudgetNearCompletion = config.autoScaleBudgetNearCompletion;
|
|
124
|
-
if (typeof config.enableTemplatesDrawer === 'boolean') this.enableTemplatesDrawer = config.enableTemplatesDrawer;
|
|
125
|
-
if (typeof config.enableMilestoneDependencies === 'boolean') this.enableMilestoneDependencies = config.enableMilestoneDependencies;
|
|
126
110
|
if (typeof config.soundScheme === 'string') this.soundScheme = config.soundScheme;
|
|
127
|
-
if (typeof config.enableVoiceAnnouncements === 'boolean') this.enableVoiceAnnouncements = config.enableVoiceAnnouncements;
|
|
128
111
|
this.emit();
|
|
129
112
|
}
|
|
130
113
|
|
|
@@ -138,14 +121,8 @@ export class GoalEngine {
|
|
|
138
121
|
this.toolFailureCounters.set(sid, next);
|
|
139
122
|
return next;
|
|
140
123
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.toolFailureCounters.set(sessionId || 'default', 0);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
getToolFailureCount(sessionId = 'default') {
|
|
147
|
-
return this.toolFailureCounters.get(sessionId || 'default') || 0;
|
|
148
|
-
}
|
|
124
|
+
resetToolFailureCount(sessionId = 'default') { this.toolFailureCounters.set(sessionId || 'default', 0); }
|
|
125
|
+
getToolFailureCount(sessionId = 'default') { return this.toolFailureCounters.get(sessionId || 'default') || 0; }
|
|
149
126
|
|
|
150
127
|
nudge(text, sessionId = 'default') {
|
|
151
128
|
const sid = sessionId || 'default';
|
|
@@ -227,6 +204,8 @@ export class GoalEngine {
|
|
|
227
204
|
preplan: null,
|
|
228
205
|
retrospective: null,
|
|
229
206
|
issueRef: parseIssueRef(cleanTitle),
|
|
207
|
+
operatorQuery: null,
|
|
208
|
+
waitingForOperator: false,
|
|
230
209
|
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
231
210
|
logs: [{
|
|
232
211
|
timestamp: now,
|
|
@@ -359,7 +338,7 @@ export class GoalEngine {
|
|
|
359
338
|
if (!target) return false;
|
|
360
339
|
|
|
361
340
|
if (status === MilestoneStatus.COMPLETED && target.verificationCommand) {
|
|
362
|
-
const vRes = verifyMilestoneAcceptance(target, goal.cwd || process.cwd(), goal, recordActivity);
|
|
341
|
+
const vRes = verifyMilestoneAcceptance(target, goal.cwd || process.cwd(), goal, recordActivity, { approvalGate: this.approvalGate, checkApproval: this.checkApproval });
|
|
363
342
|
if (!vRes.ok) {
|
|
364
343
|
this.emit(sid);
|
|
365
344
|
return false;
|
|
@@ -582,6 +561,24 @@ export class GoalEngine {
|
|
|
582
561
|
return this.goals.get(sessionId || 'default') || null;
|
|
583
562
|
}
|
|
584
563
|
|
|
564
|
+
askOperator(query = {}, sessionId = 'default') {
|
|
565
|
+
const sid = sessionId || 'default';
|
|
566
|
+
const goal = this.goals.get(sid);
|
|
567
|
+
if (!goal) return { ok: false, error: 'Goal not found' };
|
|
568
|
+
const res = askOperatorQuery(goal, query);
|
|
569
|
+
this.emit(sid, true);
|
|
570
|
+
return res;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
answerOperator(payload = {}, sessionId = 'default') {
|
|
574
|
+
const sid = sessionId || 'default';
|
|
575
|
+
const goal = this.goals.get(sid);
|
|
576
|
+
if (!goal) return { ok: false, error: 'Goal not found' };
|
|
577
|
+
const res = answerOperatorQuery(goal, payload);
|
|
578
|
+
this.emit(sid, true);
|
|
579
|
+
return res;
|
|
580
|
+
}
|
|
581
|
+
|
|
585
582
|
isPlanMode(sessionId = 'default') {
|
|
586
583
|
const goal = this.goals.get(sessionId || 'default');
|
|
587
584
|
return Boolean(goal && (goal.mode === GoalMode.PLAN || goal.state === GoalState.PLANNING));
|
package/lib/index.js
CHANGED
|
@@ -121,6 +121,7 @@ export function apply(ctx, config = {}) {
|
|
|
121
121
|
const milestoneManager = new MilestoneManager(addonStore, {
|
|
122
122
|
gitSnapshotManager,
|
|
123
123
|
enforceDependencies: currentSettings.enableMilestoneDependencies,
|
|
124
|
+
approvalGate: safeService(ctx, "approvalGate"),
|
|
124
125
|
logger,
|
|
125
126
|
});
|
|
126
127
|
const tokenBudgetGuard = new TokenBudgetGuard({
|
|
@@ -167,6 +168,7 @@ export function apply(ctx, config = {}) {
|
|
|
167
168
|
autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
168
169
|
enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
|
|
169
170
|
enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
|
|
171
|
+
approvalGate: safeService(ctx, "approvalGate"),
|
|
170
172
|
logger,
|
|
171
173
|
});
|
|
172
174
|
registerPlanModeGuard(ctx, { engine: fallbackEngine, logger });
|
package/lib/milestone-manager.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { syncMilestoneWithIssue } from './engine-issue-sync.js';
|
|
2
|
-
import { runMilestoneVerification } from './engine-verification.js';
|
|
2
|
+
import { runMilestoneVerification, validateVerificationCommand } from './engine-verification.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Milestone and checklist coordinator for active core goals
|
|
6
6
|
*/
|
|
7
7
|
export class MilestoneManager {
|
|
8
|
-
constructor(addonStore, { gitSnapshotManager = null, enforceDependencies = true, logger = console } = {}) {
|
|
8
|
+
constructor(addonStore, { gitSnapshotManager = null, enforceDependencies = true, approvalGate = null, checkApproval = null, logger = console } = {}) {
|
|
9
9
|
this.store = addonStore;
|
|
10
10
|
this.gitSnapshotManager = gitSnapshotManager;
|
|
11
11
|
this.enforceDependencies = enforceDependencies;
|
|
12
|
+
this.approvalGate = approvalGate;
|
|
13
|
+
this.checkApproval = checkApproval;
|
|
12
14
|
this.logger = logger;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -80,7 +82,10 @@ export class MilestoneManager {
|
|
|
80
82
|
|
|
81
83
|
// Acceptance Gate (Critic Verification) - Issue #163
|
|
82
84
|
if (m.verificationCommand) {
|
|
83
|
-
const vResult = runMilestoneVerification(m.verificationCommand, workspaceRoot
|
|
85
|
+
const vResult = runMilestoneVerification(m.verificationCommand, workspaceRoot, {
|
|
86
|
+
approvalGate: this.approvalGate,
|
|
87
|
+
checkApproval: this.checkApproval,
|
|
88
|
+
});
|
|
84
89
|
if (!vResult.ok) {
|
|
85
90
|
m.verificationError = vResult.error;
|
|
86
91
|
m.verificationOutput = (vResult.stderr || vResult.stdout || '').trim();
|
package/lib/routes.js
CHANGED
|
@@ -750,6 +750,30 @@ export function registerRoutes(ctx, {
|
|
|
750
750
|
break;
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
+
case 'answer_operator': {
|
|
754
|
+
const answer = typeof data.answer === 'string' ? data.answer.trim() : '';
|
|
755
|
+
const optionIndex = typeof data.optionIndex === 'number' ? data.optionIndex : (typeof data.option_index === 'number' ? data.option_index : null);
|
|
756
|
+
if (engine && typeof engine.answerOperator === 'function') {
|
|
757
|
+
const ansRes = engine.answerOperator({ answer, optionIndex }, sid);
|
|
758
|
+
if (!ansRes.ok) {
|
|
759
|
+
res.statusCode = 400;
|
|
760
|
+
return res.end(JSON.stringify({ error: ansRes.error || 'Failed to submit operator answer' }));
|
|
761
|
+
}
|
|
762
|
+
result = { ok: true, answer: ansRes.answer, state: engine.getSnapshot(sid) };
|
|
763
|
+
const snap = engine.getSnapshot(sid);
|
|
764
|
+
const detectedLang = snap?.lang || 'en';
|
|
765
|
+
const prompt = detectedLang === 'zh'
|
|
766
|
+
? `✅ 操作员已答复你的提问: "${ansRes.answer}"。请继续执行任务。`
|
|
767
|
+
: `✅ Operator answered your query: "${ansRes.answer}". Proceed with task execution.`;
|
|
768
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
769
|
+
resumeActiveAgent(prompt, sid);
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
result = { ok: true, answer };
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
|
|
753
777
|
case 'approve_plan': {
|
|
754
778
|
if (engine) {
|
|
755
779
|
const snap = engine.getSnapshot(sid);
|
package/lib/tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateVerificationCommand } from './engine-verification.js';
|
|
1
2
|
import { MilestoneManager } from './milestone-manager.js';
|
|
2
3
|
import { AddonStore } from './addon-store.js';
|
|
3
4
|
import { getSafeCwd } from './goal-engine-constants.js';
|
|
@@ -67,16 +68,30 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
if (action === 'set') {
|
|
71
|
+
if (Array.isArray(args.milestones)) {
|
|
72
|
+
for (const m of args.milestones) {
|
|
73
|
+
const vCmd = m && (m.verificationCommand || m.verification_command);
|
|
74
|
+
if (vCmd) {
|
|
75
|
+
const val = validateVerificationCommand(vCmd);
|
|
76
|
+
if (!val.valid) throw new Error(`Invalid verificationCommand: ${val.error}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
70
80
|
const list = mm.setMilestones(goalId, args.milestones);
|
|
71
81
|
return { ok: true, goalId, count: list.length, milestones: list };
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
if (action === 'add') {
|
|
85
|
+
const vCmd = args.verificationCommand || args.verification_command;
|
|
86
|
+
if (vCmd) {
|
|
87
|
+
const val = validateVerificationCommand(vCmd);
|
|
88
|
+
if (!val.valid) throw new Error(`Invalid verificationCommand: ${val.error}`);
|
|
89
|
+
}
|
|
75
90
|
const item = mm.addMilestone(goalId, {
|
|
76
91
|
title: args.title,
|
|
77
92
|
checklist: args.checklist,
|
|
78
93
|
dependsOn: args.dependsOn || args.depends_on,
|
|
79
|
-
verificationCommand:
|
|
94
|
+
verificationCommand: vCmd,
|
|
80
95
|
});
|
|
81
96
|
return { ok: true, goalId, milestone: item };
|
|
82
97
|
}
|
|
@@ -196,6 +211,11 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
196
211
|
if (typeof item === 'string') {
|
|
197
212
|
return { id: `m-${idx + 1}`, title: item };
|
|
198
213
|
}
|
|
214
|
+
const vCmd = item && (item.verificationCommand || item.verification_command);
|
|
215
|
+
if (vCmd) {
|
|
216
|
+
const val = validateVerificationCommand(vCmd);
|
|
217
|
+
if (!val.valid) throw new Error(`Invalid verificationCommand: ${val.error}`);
|
|
218
|
+
}
|
|
199
219
|
return item;
|
|
200
220
|
});
|
|
201
221
|
} else {
|
|
@@ -230,6 +250,11 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
230
250
|
const status = args.status || 'in_progress';
|
|
231
251
|
const notes = typeof args.notes === 'string' ? args.notes : '';
|
|
232
252
|
if (!milestoneId) throw new Error('milestone_id is required');
|
|
253
|
+
const vCmd = args.verificationCommand || args.verification_command;
|
|
254
|
+
if (vCmd) {
|
|
255
|
+
const val = validateVerificationCommand(vCmd);
|
|
256
|
+
if (!val.valid) throw new Error(`Invalid verificationCommand: ${val.error}`);
|
|
257
|
+
}
|
|
233
258
|
if (status === 'completed') {
|
|
234
259
|
const rawCwd = toolCtx?.session?.cwd || toolCtx?.cwd || process.cwd();
|
|
235
260
|
const safeCwd = getSafeCwd(rawCwd);
|
|
@@ -286,5 +311,70 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
286
311
|
output: JSON_OUTPUT,
|
|
287
312
|
execute: handleGoalFinish,
|
|
288
313
|
});
|
|
314
|
+
|
|
315
|
+
// Tool 6: goal_ask_operator (#166)
|
|
316
|
+
const handleGoalAskOperator = async (args = {}, toolCtx = {}) => {
|
|
317
|
+
const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
318
|
+
const question = args.question || args.prompt;
|
|
319
|
+
if (!question || typeof question !== 'string' || !question.trim()) {
|
|
320
|
+
throw new Error('Field "question" is required and must be a non-empty string');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const options = Array.isArray(args.options) ? args.options : [];
|
|
324
|
+
const isBlocking = args.isBlocking !== false && args.is_blocking !== false;
|
|
325
|
+
|
|
326
|
+
const engine = ctx.goalEngine || (ctx.get && ctx.get('goalEngine'));
|
|
327
|
+
if (engine && typeof engine.askOperator === 'function') {
|
|
328
|
+
const res = engine.askOperator({ question, options, isBlocking }, goalId);
|
|
329
|
+
if (!res.ok) {
|
|
330
|
+
throw new Error(`Failed to present question to operator: ${res.error}`);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
goalId,
|
|
335
|
+
queryId: res.queryId,
|
|
336
|
+
question: res.question,
|
|
337
|
+
options: res.options,
|
|
338
|
+
isBlocking: res.isBlocking,
|
|
339
|
+
message: isBlocking
|
|
340
|
+
? 'Question submitted to human operator. Goal is paused awaiting operator input.'
|
|
341
|
+
: 'Question submitted to human operator in non-blocking mode.',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
goalId,
|
|
348
|
+
question,
|
|
349
|
+
options,
|
|
350
|
+
message: 'Question logged.',
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
safeRegister({
|
|
355
|
+
name: 'goal_ask_operator',
|
|
356
|
+
description: 'Ask the human operator a clarification question or present architectural choices when encountering blockers (#166). Pauses autonomous execution without consuming iterations until the operator responds.',
|
|
357
|
+
parameters: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: {
|
|
360
|
+
question: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
description: 'The clarification question, design decision, or blocker to ask the human operator',
|
|
363
|
+
},
|
|
364
|
+
options: {
|
|
365
|
+
type: 'array',
|
|
366
|
+
items: { type: 'string' },
|
|
367
|
+
description: 'Optional list of predefined choices or alternative options for the operator to select with one click',
|
|
368
|
+
},
|
|
369
|
+
is_blocking: {
|
|
370
|
+
type: 'boolean',
|
|
371
|
+
description: 'Whether to pause goal execution awaiting operator response (default: true)',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
required: ['question'],
|
|
375
|
+
},
|
|
376
|
+
output: JSON_OUTPUT,
|
|
377
|
+
execute: handleGoalAskOperator,
|
|
378
|
+
});
|
|
289
379
|
});
|
|
290
380
|
}
|
package/package.json
CHANGED