@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.
@@ -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
- * Парсинг строки ввода для команды /goal
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
- * Создание валидного identified сообщения пользователя для отправки агенту через followup
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
- * Форматирование текстового ответа для UI и постановка/управление задачей агента
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: 'success' | 'error', text: string }}
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.getSnapshot(sid);
87
-
88
- const lang = (parsed.text ? detectLanguage(parsed.text) : (snap.lang || (snap.title ? detectLanguage(snap.title) : 'en'))) || 'en';
89
- const isRu = lang === 'ru';
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: isRu
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
- ? (isRu ? `\nПлан работ:\n` : `\nWork plan:\n`) + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
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
- ? (isRu
110
- ? `\nТокены: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`
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: isRu
117
- ? `🎯 Цель: «${snap.title}»\n` +
118
- `Статус: ${snap.state}\n` +
119
- `Время: ${snap.formattedElapsed}${etaText}\n` +
120
- `Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
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Команды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`
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: isRu ? 'Цель не была установлена.' : 'No active goal set.',
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
- console.warn('[dsh-goal] Failed to cancel agent on clear:', err);
136
+ // agent cancellation error handled defensively
147
137
  }
148
138
  }
149
139
  return {
150
140
  kind: 'success',
151
- text: isRu ? '🎯 Цель сброшена.' : '🎯 Goal cleared.',
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: isRu
160
- ? `Нельзя приостановить: активная цель отсутствует.\n${usage}`
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: isRu ? '⏸ Цель уже на паузе.' : '⏸ Goal is already paused.',
157
+ text: isZh ? '⏸ 目标当前已处于暂停状态。' : '⏸ Goal is already paused.',
168
158
  };
169
159
  }
170
- engine.pause(isRu ? 'Пауза по команде пользователя (/goal pause)' : 'Paused by user command (/goal pause)', sid);
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
- console.warn('[dsh-goal] Failed to cancel agent on pause:', err);
165
+ // agent cancellation error handled defensively
176
166
  }
177
167
  }
178
168
  return {
179
169
  kind: 'success',
180
- text: isRu
181
- ? `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`
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: isRu
191
- ? `Нельзя возобновить: активная цель отсутствует.\n${usage}`
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: isRu ? '▶️ Цель уже выполняется.' : '▶️ Goal is already running.',
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 = isRu
205
- ? `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`
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
- console.warn('[dsh-goal] Failed to followup agent on resume:', err);
199
+ // agent followup error handled defensively
210
200
  }
211
201
  }
212
202
  return {
213
203
  kind: 'success',
214
- text: isRu
215
- ? `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`
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 newSnap = engine.startGoal(parsed.text, { maxIterations, lang }, sid);
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, lang));
219
+ const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text, goalLang));
228
220
  agent.followup(userMsg);
229
221
  } catch (err) {
230
- console.warn('[dsh-goal] Failed to dispatch goal to agent followup:', err);
222
+ // agent dispatch error handled defensively
231
223
  }
232
224
  }
233
225
 
234
226
  return {
235
227
  kind: 'success',
236
- text: isRu
237
- ? `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
238
- `Для паузы: /goal pause | Для сброса: /goal clear`
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: isRu ? `Неизвестная команда. ${usage}` : `Unknown command. ${usage}`,
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 ? ` &nbsp;|&nbsp; **Git Start:** \`${state.gitStartCommit}\` 📌` : '';
83
+
84
+ let md = `## 🎯 Autonomous Goal Resolution: ${title}\n\n`;
85
+ md += `> **Status:** \`${status}\` 🚀 &nbsp;|&nbsp; **Duration:** \`${elapsed}\` ⏱️ &nbsp;|&nbsp; **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
+ }