@goodandready/dsh-goal 0.2.1 → 0.2.3
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 +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/card-form-state.js +65 -21
- package/lib/client.js +4384 -2249
- package/lib/command-handler.js +51 -59
- package/lib/engine-prompt.js +57 -0
- package/lib/engine-reports.js +114 -0
- package/lib/engine-store.js +142 -0
- package/lib/goal-engine-constants.js +100 -0
- package/lib/goal-engine.js +579 -1009
- package/lib/index.js +452 -980
- package/lib/routes.js +294 -0
- package/lib/tools.js +294 -0
- package/lib/updater.js +317 -0
- package/package.json +4 -5
- package/docs/design/DESIGN.md +0 -184
package/lib/command-handler.js
CHANGED
|
@@ -3,11 +3,9 @@ import { GoalState, detectLanguage } from './goal-engine.js';
|
|
|
3
3
|
|
|
4
4
|
export const USAGE_EN = 'Usage: /goal [<goal>|clear|pause|resume]';
|
|
5
5
|
export const USAGE_ZH = '用法: /goal [<目标描述>|clear|pause|resume]';
|
|
6
|
-
export const USAGE_RU = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
7
|
-
export const USAGE = USAGE_EN;
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
8
|
+
* Parse input string for /goal command
|
|
11
9
|
* @param {string} rawInput
|
|
12
10
|
* @returns {{ action: string, text?: string }}
|
|
13
11
|
*/
|
|
@@ -26,7 +24,7 @@ export function parseGoalInput(rawInput = '') {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
|
-
*
|
|
27
|
+
* Create a valid identified user message for agent followup
|
|
30
28
|
* @param {string} text
|
|
31
29
|
* @returns {Object}
|
|
32
30
|
*/
|
|
@@ -40,7 +38,7 @@ export function createGoalUserMessage(text) {
|
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
/**
|
|
43
|
-
*
|
|
41
|
+
* Format goal start prompt with mandatory milestone contract
|
|
44
42
|
* @param {string} goalText
|
|
45
43
|
* @param {string} [explicitLang]
|
|
46
44
|
* @returns {string}
|
|
@@ -55,14 +53,6 @@ export function formatGoalStartPrompt(goalText, explicitLang) {
|
|
|
55
53
|
`3. 第三步:完成某一步骤后,通过 goal_update_progress(milestone_id, "completed", "阶段简要成果") 标记为已完成。\n` +
|
|
56
54
|
`4. 第四步:当所有里程碑全部完成后,调用 goal_finish(summary) 工具并附带详尽的最终成果总结。`;
|
|
57
55
|
}
|
|
58
|
-
if (lang === 'ru') {
|
|
59
|
-
return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
|
|
60
|
-
`СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
|
|
61
|
-
`1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
62
|
-
`2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
|
|
63
|
-
`3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
|
|
64
|
-
`4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
|
|
65
|
-
}
|
|
66
56
|
|
|
67
57
|
return `🎯 Goal Mode activated: "${goalText}"\n\n` +
|
|
68
58
|
`STRICT AUTONOMOUS CONTRACT:\n` +
|
|
@@ -73,54 +63,54 @@ export function formatGoalStartPrompt(goalText, explicitLang) {
|
|
|
73
63
|
}
|
|
74
64
|
|
|
75
65
|
/**
|
|
76
|
-
*
|
|
66
|
+
* Format response for UI and manage agent task execution
|
|
77
67
|
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
78
68
|
* @param {{ action: string, text?: string }} parsed
|
|
79
69
|
* @param {Object} [config]
|
|
80
70
|
* @param {Object} [agent]
|
|
81
71
|
* @param {string} [sessionId='default']
|
|
82
|
-
* @returns {{ kind:
|
|
72
|
+
* @returns {{ kind: string, text: string }}
|
|
83
73
|
*/
|
|
84
74
|
export function executeGoalSlashCommand(engine, parsed, config = {}, agent = null, sessionId = 'default') {
|
|
85
75
|
const sid = sessionId || 'default';
|
|
86
|
-
const snap = engine.
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const usage = isRu ? USAGE_RU : USAGE_EN;
|
|
76
|
+
const snap = engine.getGoalSnapshot(sid);
|
|
77
|
+
const lang = snap.lang || (parsed.text ? detectLanguage(parsed.text) : 'en');
|
|
78
|
+
const isZh = lang === 'zh';
|
|
79
|
+
const usage = isZh ? USAGE_ZH : USAGE_EN;
|
|
91
80
|
|
|
92
81
|
switch (parsed.action) {
|
|
93
82
|
case 'show': {
|
|
94
83
|
if (!snap.hasActiveGoal) {
|
|
95
84
|
return {
|
|
96
85
|
kind: 'success',
|
|
97
|
-
text:
|
|
98
|
-
? `🎯 Режим цели (Goal Mode): цель не установлена.\n${usage}`
|
|
99
|
-
: `🎯 Goal Mode: no active goal set.\n${usage}`,
|
|
86
|
+
text: isZh ? `目标模式: 当前未设置活动目标。\n${usage}` : `Goal Mode: no active goal set.\n${usage}`,
|
|
100
87
|
};
|
|
101
88
|
}
|
|
102
89
|
|
|
103
|
-
const milestonesInfo = snap.milestones.length > 0
|
|
104
|
-
?
|
|
90
|
+
const milestonesInfo = snap.milestones && snap.milestones.length > 0
|
|
91
|
+
? `\n\n${isZh ? '工作计划' : 'Work Plan'}:\n` +
|
|
92
|
+
snap.milestones
|
|
93
|
+
.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`)
|
|
94
|
+
.join('\n')
|
|
105
95
|
: '';
|
|
106
96
|
|
|
107
97
|
const etaText = snap.formattedETA ? ` (ETA: ${snap.formattedETA})` : '';
|
|
108
98
|
const tokensInfo = (snap.tokensUsage && snap.tokensUsage.totalTokens > 0)
|
|
109
|
-
? (
|
|
110
|
-
? `\
|
|
99
|
+
? (isZh
|
|
100
|
+
? `\nToken: ${snap.tokensUsage.totalTokens.toLocaleString()} (输入: ${snap.tokensUsage.promptTokens.toLocaleString()}, 输出: ${snap.tokensUsage.completionTokens.toLocaleString()})`
|
|
111
101
|
: `\nTokens: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`)
|
|
112
102
|
: '';
|
|
113
103
|
|
|
114
104
|
return {
|
|
115
105
|
kind: 'success',
|
|
116
|
-
text:
|
|
117
|
-
? `🎯
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
text: isZh
|
|
107
|
+
? `🎯 目标: "${snap.title}"\n` +
|
|
108
|
+
`状态: ${snap.state}\n` +
|
|
109
|
+
`耗时: ${snap.formattedElapsed}${etaText}\n` +
|
|
110
|
+
`迭代轮次: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
121
111
|
tokensInfo +
|
|
122
112
|
milestonesInfo +
|
|
123
|
-
`\n\n
|
|
113
|
+
`\n\n快捷指令: /goal pause, /goal resume, /goal clear, /goal <新目标>`
|
|
124
114
|
: `🎯 Goal: "${snap.title}"\n` +
|
|
125
115
|
`Status: ${snap.state}\n` +
|
|
126
116
|
`Duration: ${snap.formattedElapsed}${etaText}\n` +
|
|
@@ -135,7 +125,7 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
135
125
|
if (!snap.hasActiveGoal) {
|
|
136
126
|
return {
|
|
137
127
|
kind: 'success',
|
|
138
|
-
text:
|
|
128
|
+
text: isZh ? '当前未设置目标。' : 'No active goal set.',
|
|
139
129
|
};
|
|
140
130
|
}
|
|
141
131
|
engine.clear(sid);
|
|
@@ -143,12 +133,12 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
143
133
|
try {
|
|
144
134
|
agent.cancel({ kind: 'user', reason: 'Goal cleared by user' });
|
|
145
135
|
} catch (err) {
|
|
146
|
-
|
|
136
|
+
// agent cancellation error handled defensively
|
|
147
137
|
}
|
|
148
138
|
}
|
|
149
139
|
return {
|
|
150
140
|
kind: 'success',
|
|
151
|
-
text:
|
|
141
|
+
text: isZh ? '🎯 目标已清除。' : '🎯 Goal cleared.',
|
|
152
142
|
};
|
|
153
143
|
}
|
|
154
144
|
|
|
@@ -156,29 +146,29 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
156
146
|
if (!snap.hasActiveGoal) {
|
|
157
147
|
return {
|
|
158
148
|
kind: 'error',
|
|
159
|
-
text:
|
|
160
|
-
?
|
|
149
|
+
text: isZh
|
|
150
|
+
? `无法暂停:未设置活动目标。\n${usage}`
|
|
161
151
|
: `Cannot pause: no active goal.\n${usage}`,
|
|
162
152
|
};
|
|
163
153
|
}
|
|
164
154
|
if (snap.state === GoalState.PAUSED) {
|
|
165
155
|
return {
|
|
166
156
|
kind: 'success',
|
|
167
|
-
text:
|
|
157
|
+
text: isZh ? '⏸ 目标当前已处于暂停状态。' : '⏸ Goal is already paused.',
|
|
168
158
|
};
|
|
169
159
|
}
|
|
170
|
-
engine.pause(
|
|
160
|
+
engine.pause(isZh ? '用户命令暂停 (/goal pause)' : 'Paused by user command (/goal pause)', sid);
|
|
171
161
|
if (agent && typeof agent.cancel === 'function') {
|
|
172
162
|
try {
|
|
173
163
|
agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
|
|
174
164
|
} catch (err) {
|
|
175
|
-
|
|
165
|
+
// agent cancellation error handled defensively
|
|
176
166
|
}
|
|
177
167
|
}
|
|
178
168
|
return {
|
|
179
169
|
kind: 'success',
|
|
180
|
-
text:
|
|
181
|
-
? `⏸
|
|
170
|
+
text: isZh
|
|
171
|
+
? `⏸ 目标已暂停: "${snap.title}"。智能体已停止,使用 /goal resume 或 ▶️ 按钮继续。`
|
|
182
172
|
: `⏸ Goal paused: "${snap.title}". Agent stopped. Use /goal resume or ▶️ button to continue.`,
|
|
183
173
|
};
|
|
184
174
|
}
|
|
@@ -187,55 +177,57 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
187
177
|
if (!snap.hasActiveGoal) {
|
|
188
178
|
return {
|
|
189
179
|
kind: 'error',
|
|
190
|
-
text:
|
|
191
|
-
?
|
|
180
|
+
text: isZh
|
|
181
|
+
? `无法恢复:未设置活动目标。\n${usage}`
|
|
192
182
|
: `Cannot resume: no active goal.\n${usage}`,
|
|
193
183
|
};
|
|
194
184
|
}
|
|
195
185
|
if (snap.state === GoalState.RUNNING) {
|
|
196
186
|
return {
|
|
197
187
|
kind: 'success',
|
|
198
|
-
text:
|
|
188
|
+
text: isZh ? '▶️ 目标已在运行中。' : '▶️ Goal is already running.',
|
|
199
189
|
};
|
|
200
190
|
}
|
|
201
191
|
engine.resume(sid);
|
|
202
192
|
if (agent && typeof agent.followup === 'function') {
|
|
203
193
|
try {
|
|
204
|
-
const resumePrompt =
|
|
205
|
-
? `▶️
|
|
194
|
+
const resumePrompt = isZh
|
|
195
|
+
? `▶️ 用户已恢复目标运行。请继续推进工作计划中尚未完成的步骤。通过 goal_update_progress 实时更新进度。`
|
|
206
196
|
: `▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.`;
|
|
207
197
|
agent.followup(createGoalUserMessage(resumePrompt));
|
|
208
198
|
} catch (err) {
|
|
209
|
-
|
|
199
|
+
// agent followup error handled defensively
|
|
210
200
|
}
|
|
211
201
|
}
|
|
212
202
|
return {
|
|
213
203
|
kind: 'success',
|
|
214
|
-
text:
|
|
215
|
-
? `▶️
|
|
204
|
+
text: isZh
|
|
205
|
+
? `▶️ 目标已恢复: "${snap.title}"。智能体继续工作中。`
|
|
216
206
|
: `▶️ Goal resumed: "${snap.title}". Agent continues working.`,
|
|
217
207
|
};
|
|
218
208
|
}
|
|
219
209
|
|
|
220
210
|
case 'start': {
|
|
211
|
+
const goalLang = detectLanguage(parsed.text);
|
|
212
|
+
const isZhStart = goalLang === 'zh';
|
|
221
213
|
const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
|
|
222
|
-
const
|
|
214
|
+
const cwd = config.cwd;
|
|
215
|
+
const newSnap = engine.startGoal(parsed.text, { maxIterations, lang: goalLang, cwd }, sid);
|
|
223
216
|
|
|
224
|
-
// Если команда вызвана в контексте агента, отправляем цель в очередь модели
|
|
225
217
|
if (agent && typeof agent.followup === 'function') {
|
|
226
218
|
try {
|
|
227
|
-
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text,
|
|
219
|
+
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text, goalLang));
|
|
228
220
|
agent.followup(userMsg);
|
|
229
221
|
} catch (err) {
|
|
230
|
-
|
|
222
|
+
// agent dispatch error handled defensively
|
|
231
223
|
}
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
return {
|
|
235
227
|
kind: 'success',
|
|
236
|
-
text:
|
|
237
|
-
? `🎯
|
|
238
|
-
|
|
228
|
+
text: isZhStart
|
|
229
|
+
? `🎯 目标已激活: "${newSnap.title}" (上限 ${newSnap.maxIterations} 轮迭代)。\n` +
|
|
230
|
+
`暂停: /goal pause | 清除: /goal clear`
|
|
239
231
|
: `🎯 Goal activated: "${newSnap.title}" (max ${newSnap.maxIterations} iterations).\n` +
|
|
240
232
|
`To pause: /goal pause | To clear: /goal clear`,
|
|
241
233
|
};
|
|
@@ -244,7 +236,7 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
244
236
|
default:
|
|
245
237
|
return {
|
|
246
238
|
kind: 'error',
|
|
247
|
-
text:
|
|
239
|
+
text: isZh ? `未知指令。${usage}` : `Unknown command. ${usage}`,
|
|
248
240
|
};
|
|
249
241
|
}
|
|
250
242
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt generation for DSH Goal Mode
|
|
3
|
+
*/
|
|
4
|
+
export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
5
|
+
const lang = snapshot.lang || 'en';
|
|
6
|
+
const hasMilestones = snapshot.milestones && snapshot.milestones.length > 0;
|
|
7
|
+
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
8
|
+
|
|
9
|
+
let nudgeText = '';
|
|
10
|
+
if (pendingNudge) {
|
|
11
|
+
if (lang === 'zh') {
|
|
12
|
+
nudgeText = `\n\n🚨 用户紧急补充说明 / 调整方向:\n"${pendingNudge}"\n你必须根据此说明调整近期的具体执行步骤!\n`;
|
|
13
|
+
} else {
|
|
14
|
+
nudgeText = `\n\n🚨 URGENT USER CLARIFICATION / STEERING:\n"${pendingNudge}"\nYou must adjust your immediate actions according to this guidance!\n`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (lang === 'zh') {
|
|
19
|
+
const milestonesText = hasMilestones
|
|
20
|
+
? snapshot.milestones
|
|
21
|
+
.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`)
|
|
22
|
+
.join('\n')
|
|
23
|
+
: ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
nudgeText +
|
|
27
|
+
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
28
|
+
`目标: "${snapshot.title}"\n` +
|
|
29
|
+
`运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
30
|
+
`工作计划:\n` +
|
|
31
|
+
`${milestonesText}\n\n` +
|
|
32
|
+
`Goal Mode 执行契约 (必须严格遵循):\n` +
|
|
33
|
+
`1. ${hasMilestones ? '按部就班推进当前进行中的里程碑。' : '第一步核心指令: 立即调用 goal_set_milestones 制定 3-7 个具体里程碑。在完成此工具调用前严禁执行其他操作!'}\n` +
|
|
34
|
+
`2. 推进里程碑时,必须通过 goal_update_progress 工具更新状态(开始前标为 in_progress,完成后标为 completed 并附简要说明)。\n` +
|
|
35
|
+
`3. 当所有里程碑全部完成后,调用 goal_finish 工具提交详细成果总结。`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const milestonesText = hasMilestones
|
|
40
|
+
? snapshot.milestones
|
|
41
|
+
.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`)
|
|
42
|
+
.join('\n')
|
|
43
|
+
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
nudgeText +
|
|
47
|
+
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
48
|
+
`Goal: "${snapshot.title}"\n` +
|
|
49
|
+
`Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
50
|
+
`Work Plan:\n` +
|
|
51
|
+
`${milestonesText}\n\n` +
|
|
52
|
+
`Goal Mode Instructions (MANDATORY TO FOLLOW):\n` +
|
|
53
|
+
`1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}\n` +
|
|
54
|
+
`2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes).\n` +
|
|
55
|
+
`3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely get current short git commit hash
|
|
5
|
+
* @param {string} [cwd]
|
|
6
|
+
* @returns {string|null}
|
|
7
|
+
*/
|
|
8
|
+
export function getGitCurrentCommit(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
return execSync('git rev-parse --short HEAD', {
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
timeout: 1000,
|
|
14
|
+
cwd: cwd || undefined,
|
|
15
|
+
}).trim();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate Markdown report for goal results
|
|
23
|
+
* @param {Object} state
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function exportReportMarkdown(state) {
|
|
27
|
+
if (!state) return '';
|
|
28
|
+
const title = state.title || 'Goal Report';
|
|
29
|
+
const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
|
|
30
|
+
const elapsed = state.formattedElapsed || '0s';
|
|
31
|
+
const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
|
|
32
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
33
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
34
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
35
|
+
const gitCommit = state.gitStartCommit ? ` | **Git Start:** \`${state.gitStartCommit}\`` : '';
|
|
36
|
+
|
|
37
|
+
let md = `# 🎯 Goal Report: ${title}\n\n`;
|
|
38
|
+
md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`${gitCommit}\n`;
|
|
39
|
+
if (totalTokens > 0) {
|
|
40
|
+
md += `**Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n`;
|
|
41
|
+
}
|
|
42
|
+
md += '\n';
|
|
43
|
+
|
|
44
|
+
if (state.description) {
|
|
45
|
+
md += `### Description\n${state.description}\n\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (state.resultSummary) {
|
|
49
|
+
md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const milestones = state.milestones || [];
|
|
53
|
+
if (milestones.length > 0) {
|
|
54
|
+
md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
|
|
55
|
+
milestones.forEach((m, idx) => {
|
|
56
|
+
const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
|
|
57
|
+
const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
|
|
58
|
+
const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
|
|
59
|
+
md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
|
|
60
|
+
});
|
|
61
|
+
md += '\n';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
|
|
65
|
+
return md;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate GitHub / Gitea PR comment report with collapsible details
|
|
70
|
+
* @param {Object} state
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function exportReportGitHubPR(state) {
|
|
74
|
+
if (!state) return '';
|
|
75
|
+
const title = state.title || 'Goal Report';
|
|
76
|
+
const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
|
|
77
|
+
const elapsed = state.formattedElapsed || '0s';
|
|
78
|
+
const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
|
|
79
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
80
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
81
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
82
|
+
const gitCommit = state.gitStartCommit ? ` | **Git Start:** \`${state.gitStartCommit}\` 📌` : '';
|
|
83
|
+
|
|
84
|
+
let md = `## 🎯 Autonomous Goal Resolution: ${title}\n\n`;
|
|
85
|
+
md += `> **Status:** \`${status}\` 🚀 | **Duration:** \`${elapsed}\` ⏱️ | **Iterations:** \`${iter}\` 🔄${gitCommit}\n\n`;
|
|
86
|
+
|
|
87
|
+
if (totalTokens > 0) {
|
|
88
|
+
md += `### 📊 Telemetry & Token Usage\n`;
|
|
89
|
+
md += `- **Total Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (state.resultSummary) {
|
|
93
|
+
md += `### 📦 Deliverables & Achievements\n${state.resultSummary}\n\n`;
|
|
94
|
+
} else if (state.description) {
|
|
95
|
+
md += `### 📝 Objective\n${state.description}\n\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const milestones = state.milestones || [];
|
|
99
|
+
if (milestones.length > 0) {
|
|
100
|
+
const completedCount = milestones.filter((m) => m.status === 'completed').length;
|
|
101
|
+
md += `<details>\n<summary><b>📋 Milestones Breakdown (${completedCount}/${milestones.length} Completed)</b></summary>\n\n`;
|
|
102
|
+
md += `| # | Status | Milestone | Notes |\n|---|---|---|---|\n`;
|
|
103
|
+
milestones.forEach((m, idx) => {
|
|
104
|
+
const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
|
|
105
|
+
const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
|
|
106
|
+
const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
|
|
107
|
+
md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
|
|
108
|
+
});
|
|
109
|
+
md += `\n</details>\n\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
md += `*Automated by [@goodandready/dsh-goal](https://github.com/GooDAnDReaDY/dsh-goal)*\n`;
|
|
113
|
+
return md;
|
|
114
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { GoalState } from './goal-engine-constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage manager for goal engine disk state persistence
|
|
7
|
+
*/
|
|
8
|
+
export class EngineStore {
|
|
9
|
+
constructor(engine, storagePath) {
|
|
10
|
+
this.engine = engine;
|
|
11
|
+
this.storagePath = storagePath || null;
|
|
12
|
+
this.saveTimer = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setStoragePath(storagePath) {
|
|
16
|
+
this.storagePath = storagePath || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
loadStateFromDisk() {
|
|
20
|
+
if (!this.storagePath) return;
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(this.storagePath)) {
|
|
23
|
+
const raw = fs.readFileSync(this.storagePath, 'utf8');
|
|
24
|
+
const data = JSON.parse(raw);
|
|
25
|
+
if (data && typeof data === 'object') {
|
|
26
|
+
let dirty = false;
|
|
27
|
+
if (data.sessions && typeof data.sessions === 'object') {
|
|
28
|
+
for (const [sid, goal] of Object.entries(data.sessions)) {
|
|
29
|
+
if (goal && goal.id && goal.title) {
|
|
30
|
+
// Crash Hydration: transition leftover RUNNING goals to PAUSED
|
|
31
|
+
if (goal.state === GoalState.RUNNING) {
|
|
32
|
+
goal.state = GoalState.PAUSED;
|
|
33
|
+
goal.pausedAt = Date.now();
|
|
34
|
+
if (!Array.isArray(goal.logs)) goal.logs = [];
|
|
35
|
+
goal.logs.push({
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
type: 'warning',
|
|
38
|
+
message: 'Harness was restarted — click ▶️ to resume',
|
|
39
|
+
});
|
|
40
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
41
|
+
dirty = true;
|
|
42
|
+
}
|
|
43
|
+
if (!goal.tokensUsage) {
|
|
44
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
45
|
+
}
|
|
46
|
+
if (!goal.lang) {
|
|
47
|
+
goal.lang = 'en';
|
|
48
|
+
}
|
|
49
|
+
this.engine.goals.set(sid, goal);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else if (data.id && data.title) {
|
|
53
|
+
if (data.state === GoalState.RUNNING) {
|
|
54
|
+
data.state = GoalState.PAUSED;
|
|
55
|
+
data.pausedAt = Date.now();
|
|
56
|
+
if (!Array.isArray(data.logs)) data.logs = [];
|
|
57
|
+
data.logs.push({
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
type: 'warning',
|
|
60
|
+
message: 'Harness was restarted — click ▶️ to resume',
|
|
61
|
+
});
|
|
62
|
+
if (data.logs.length > 100) data.logs = data.logs.slice(-100);
|
|
63
|
+
dirty = true;
|
|
64
|
+
}
|
|
65
|
+
if (!data.tokensUsage) {
|
|
66
|
+
data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
67
|
+
}
|
|
68
|
+
if (!data.lang) {
|
|
69
|
+
data.lang = 'en';
|
|
70
|
+
}
|
|
71
|
+
this.engine.goals.set('default', data);
|
|
72
|
+
}
|
|
73
|
+
if (dirty) {
|
|
74
|
+
this.scheduleSave(true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// best-effort load failure logged defensively
|
|
80
|
+
console.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
scheduleSave(immediate = false) {
|
|
85
|
+
if (!this.storagePath) return;
|
|
86
|
+
if (immediate) {
|
|
87
|
+
if (this.saveTimer) {
|
|
88
|
+
clearTimeout(this.saveTimer);
|
|
89
|
+
this.saveTimer = null;
|
|
90
|
+
}
|
|
91
|
+
this.writeStateToDiskSync();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!this.saveTimer) {
|
|
95
|
+
this.saveTimer = setTimeout(() => {
|
|
96
|
+
this.saveTimer = null;
|
|
97
|
+
this.writeStateToDiskSync();
|
|
98
|
+
}, 250);
|
|
99
|
+
if (typeof this.saveTimer.unref === 'function') {
|
|
100
|
+
this.saveTimer.unref();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writeStateToDiskSync() {
|
|
106
|
+
if (!this.storagePath) return;
|
|
107
|
+
try {
|
|
108
|
+
if (this.engine.goals.size === 0) {
|
|
109
|
+
if (fs.existsSync(this.storagePath)) {
|
|
110
|
+
fs.unlinkSync(this.storagePath);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const sessionsObj = {};
|
|
115
|
+
for (const [sid, goal] of this.engine.goals.entries()) {
|
|
116
|
+
sessionsObj[sid] = goal;
|
|
117
|
+
}
|
|
118
|
+
const payload = {
|
|
119
|
+
version: 2,
|
|
120
|
+
sessions: sessionsObj,
|
|
121
|
+
...(this.engine.goals.has('default') ? this.engine.goals.get('default') : {}),
|
|
122
|
+
};
|
|
123
|
+
const dir = path.dirname(this.storagePath);
|
|
124
|
+
if (!fs.existsSync(dir)) {
|
|
125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
const tmp = `${this.storagePath}.tmp.${Date.now()}`;
|
|
128
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
129
|
+
fs.renameSync(tmp, this.storagePath);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
flushSync() {
|
|
136
|
+
if (this.saveTimer) {
|
|
137
|
+
clearTimeout(this.saveTimer);
|
|
138
|
+
this.saveTimer = null;
|
|
139
|
+
}
|
|
140
|
+
this.writeStateToDiskSync();
|
|
141
|
+
}
|
|
142
|
+
}
|