@goodandready/dsh-goal 0.2.11 → 0.2.13

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.
@@ -0,0 +1,213 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Lightweight JSON persistence for dsh-goal addon metadata:
6
+ * milestones, checklists, snapshots, and token budget metrics.
7
+ */
8
+ export class AddonStore {
9
+ constructor(storagePath, logger = console) {
10
+ this.storagePath = storagePath || null;
11
+ this.logger = logger;
12
+ this.goals = new Map(); // goalId -> { milestones, snapshots, budget, issue }
13
+ this.saveTimer = null;
14
+ this.loadFromDisk();
15
+ }
16
+
17
+ setStoragePath(storagePath) {
18
+ this.storagePath = storagePath || null;
19
+ this.loadFromDisk();
20
+ }
21
+
22
+ getGoalData(goalId) {
23
+ if (!goalId) return null;
24
+ if (!this.goals.has(goalId)) {
25
+ this.goals.set(goalId, {
26
+ milestones: [],
27
+ snapshots: [],
28
+ budget: { maxTokenBudget: 0, warnedAt: null, blockedAt: null },
29
+ issue: null,
30
+ });
31
+ }
32
+ return this.goals.get(goalId);
33
+ }
34
+
35
+ getMilestones(goalId) {
36
+ const data = this.getGoalData(goalId);
37
+ return data ? data.milestones : [];
38
+ }
39
+
40
+ setMilestones(goalId, milestones) {
41
+ const data = this.getGoalData(goalId);
42
+ if (!data) return;
43
+ data.milestones = Array.isArray(milestones) ? milestones : [];
44
+ this.scheduleSave();
45
+ }
46
+
47
+ getSnapshots(goalId) {
48
+ const data = this.getGoalData(goalId);
49
+ return data ? data.snapshots : [];
50
+ }
51
+
52
+ addSnapshot(goalId, snapshot) {
53
+ const data = this.getGoalData(goalId);
54
+ if (!data) return;
55
+ if (!Array.isArray(data.snapshots)) data.snapshots = [];
56
+ data.snapshots.push(snapshot);
57
+ this.scheduleSave();
58
+ }
59
+
60
+ getBudget(goalId) {
61
+ const data = this.getGoalData(goalId);
62
+ return data ? data.budget : null;
63
+ }
64
+
65
+ setBudget(goalId, budget) {
66
+ const data = this.getGoalData(goalId);
67
+ if (!data) return;
68
+ data.budget = { ...(data.budget || {}), ...budget };
69
+ this.scheduleSave();
70
+ }
71
+
72
+ loadFromDisk() {
73
+ if (!this.storagePath) return;
74
+ try {
75
+ if (fs.existsSync(this.storagePath)) {
76
+ const raw = fs.readFileSync(this.storagePath, 'utf8');
77
+ const parsed = JSON.parse(raw);
78
+ if (parsed && typeof parsed === 'object' && parsed.goals) {
79
+ for (const [gid, val] of Object.entries(parsed.goals)) {
80
+ this.goals.set(gid, {
81
+ milestones: Array.isArray(val.milestones) ? val.milestones : [],
82
+ snapshots: Array.isArray(val.snapshots) ? val.snapshots : [],
83
+ budget: val.budget || { maxTokenBudget: 0, warnedAt: null, blockedAt: null },
84
+ issue: val.issue || null,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ } catch (err) {
90
+ this.logger.warn?.('[AddonStore] Failed to load addon state from disk:', err?.message || err);
91
+ }
92
+ }
93
+
94
+ scheduleSave(immediate = false) {
95
+ if (!this.storagePath) return;
96
+ if (immediate) {
97
+ if (this.saveTimer) {
98
+ clearTimeout(this.saveTimer);
99
+ this.saveTimer = null;
100
+ }
101
+ this.writeToDiskSync();
102
+ return;
103
+ }
104
+ if (!this.saveTimer) {
105
+ this.saveTimer = setTimeout(() => {
106
+ this.saveTimer = null;
107
+ this.writeToDiskSync();
108
+ }, 200);
109
+ if (typeof this.saveTimer.unref === 'function') {
110
+ this.saveTimer.unref();
111
+ }
112
+ }
113
+ }
114
+
115
+ writeToDiskSync() {
116
+ if (!this.storagePath) return;
117
+ try {
118
+ const goalsObj = {};
119
+ for (const [gid, val] of this.goals.entries()) {
120
+ goalsObj[gid] = val;
121
+ }
122
+ const payload = {
123
+ version: 3,
124
+ goals: goalsObj,
125
+ updatedAt: Date.now(),
126
+ };
127
+ const dir = path.dirname(this.storagePath);
128
+ if (!fs.existsSync(dir)) {
129
+ fs.mkdirSync(dir, { recursive: true });
130
+ }
131
+ const tmp = `${this.storagePath}.tmp.${Date.now()}`;
132
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
133
+ fs.renameSync(tmp, this.storagePath);
134
+ } catch (err) {
135
+ this.logger.warn?.('[AddonStore] Failed to write addon state to disk:', err?.message || err);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Migrate legacy state from dsh-goal-state.json to addon format and core GoalService
141
+ */
142
+ migrateLegacyState(legacyPath, coreGoals, agent) {
143
+ if (!legacyPath || !fs.existsSync(legacyPath)) return false;
144
+ try {
145
+ const raw = fs.readFileSync(legacyPath, 'utf8');
146
+ const data = JSON.parse(raw);
147
+ if (!data || typeof data !== 'object' || data._migratedToAddon) return false;
148
+
149
+ let migratedAny = false;
150
+ const candidates = [];
151
+ if (data.sessions && typeof data.sessions === 'object') {
152
+ for (const [sid, goal] of Object.entries(data.sessions)) {
153
+ if (goal?.title && goal?.hasActiveGoal) {
154
+ candidates.push({ sessionId: sid, goal });
155
+ }
156
+ }
157
+ } else if (data.title && data.hasActiveGoal) {
158
+ candidates.push({ sessionId: 'default', goal: data });
159
+ }
160
+
161
+ for (const item of candidates) {
162
+ const legacyGoal = item.goal;
163
+ const goalId = legacyGoal.id || `legacy-${Date.now()}`;
164
+
165
+ // Import milestones into addon store
166
+ if (Array.isArray(legacyGoal.milestones) && legacyGoal.milestones.length > 0) {
167
+ this.setMilestones(goalId, legacyGoal.milestones.map((m, idx) => ({
168
+ id: m.id || `m-${idx + 1}`,
169
+ title: m.title || `Milestone ${idx + 1}`,
170
+ completed: m.status === 'completed',
171
+ completedAt: m.completedAt || null,
172
+ notes: m.notes || '',
173
+ dependsOn: Array.isArray(m.dependsOn) ? m.dependsOn : [],
174
+ checklist: Array.isArray(m.checklist) ? m.checklist : [],
175
+ })));
176
+ }
177
+
178
+ // If coreGoals is available and agent session is active, bridge to core
179
+ if (coreGoals && agent && typeof coreGoals.create === 'function') {
180
+ try {
181
+ const current = coreGoals.get ? coreGoals.get(agent) : null;
182
+ if (!current) {
183
+ coreGoals.create(agent, {
184
+ objective: legacyGoal.title,
185
+ maxGoalRounds: legacyGoal.maxIterations || 25,
186
+ });
187
+ }
188
+ } catch (e) {
189
+ this.logger.warn?.('[AddonStore] Migration: could not create goal in core:', e.message);
190
+ }
191
+ }
192
+ migratedAny = true;
193
+ }
194
+
195
+ data._migratedToAddon = true;
196
+ data._migratedAt = Date.now();
197
+ fs.writeFileSync(legacyPath, JSON.stringify(data, null, 2), 'utf8');
198
+ this.scheduleSave(true);
199
+ return migratedAny;
200
+ } catch (err) {
201
+ this.logger.warn?.('[AddonStore] Legacy migration failed:', err.message);
202
+ return false;
203
+ }
204
+ }
205
+
206
+ flushSync() {
207
+ if (this.saveTimer) {
208
+ clearTimeout(this.saveTimer);
209
+ this.saveTimer = null;
210
+ }
211
+ this.writeToDiskSync();
212
+ }
213
+ }
package/lib/client.js CHANGED
@@ -1609,7 +1609,7 @@ window.__ModuleLoader__.load({
1609
1609
  if (!AudioCtx) return;
1610
1610
  const actx = new AudioCtx();
1611
1611
  if (actx.state === 'suspended') {
1612
- actx.resume().catch(() => {});
1612
+ actx.resume().catch((err) => { console.warn('[dsh-goal] Fetch templates error:', err); });
1613
1613
  }
1614
1614
  const now = actx.currentTime;
1615
1615
  if (scheme === 'retro') {
@@ -2590,9 +2590,51 @@ window.__ModuleLoader__.load({
2590
2590
  function QuickLaunchModal({ onClose, onStart, t }) {
2591
2591
 
2592
2592
  const [title, setTitle] = useState('');
2593
+ const [showTemplates, setShowTemplates] = useState(false);
2594
+ const [backendTemplates, setBackendTemplates] = useState([]);
2595
+ const [selectedTpl, setSelectedTpl] = useState(null);
2596
+ const [tplInputs, setTplInputs] = useState({});
2597
+ const [loadingTpls, setLoadingTpls] = useState(false);
2593
2598
 
2594
2599
  const inputRef = useRef(null);
2595
2600
 
2601
+ useEffect(() => {
2602
+ if (showTemplates && backendTemplates.length === 0) {
2603
+ setLoadingTpls(true);
2604
+ fetch('/dsh-goal/templates')
2605
+ .then((r) => r.json())
2606
+ .then((d) => {
2607
+ if (d && Array.isArray(d.templates)) {
2608
+ setBackendTemplates(d.templates);
2609
+ }
2610
+ })
2611
+ .catch((err) => { console.warn('[dsh-goal] Fetch templates error:', err); })
2612
+ .finally(() => setLoadingTpls(false));
2613
+ }
2614
+ }, [showTemplates, backendTemplates.length]);
2615
+
2616
+ const handleApplyTemplate = async () => {
2617
+ if (!selectedTpl) return;
2618
+ try {
2619
+ const res = await fetch('/dsh-goal/action', {
2620
+ method: 'POST',
2621
+ headers: { 'Content-Type': 'application/json' },
2622
+ body: JSON.stringify({
2623
+ action: 'instantiate_template',
2624
+ templateId: selectedTpl.id,
2625
+ inputs: tplInputs,
2626
+ }),
2627
+ });
2628
+ if (res.ok) {
2629
+ const data = await res.json();
2630
+ if (data.ok && data.template) {
2631
+ setTitle(data.template.title);
2632
+ setShowTemplates(false);
2633
+ }
2634
+ }
2635
+ } catch (err) { console.warn('[dsh-goal] Apply template error:', err); }
2636
+ };
2637
+
2596
2638
  useEffect(() => {
2597
2639
 
2598
2640
  if (inputRef.current) inputRef.current.focus();
@@ -2782,8 +2824,94 @@ window.__ModuleLoader__.load({
2782
2824
  },
2783
2825
  tpl.label
2784
2826
  )
2827
+ ),
2828
+ React.createElement(
2829
+ 'button',
2830
+ {
2831
+ type: 'button',
2832
+ className: 'dsh-goal-chip-btn',
2833
+ style: { fontWeight: 600, borderColor: 'var(--dsw-alias-border-brand, var(--dsw-alias-border-l2))' },
2834
+ onClick: () => setShowTemplates(!showTemplates),
2835
+ },
2836
+ (showTemplates ? '▲ ' : '▼ ') + '📋 ' + (t('templatesTitle') || 'Engineering Templates')
2785
2837
  )
2786
2838
  ),
2839
+ showTemplates
2840
+ ? React.createElement(
2841
+ 'div',
2842
+ {
2843
+ style: {
2844
+ marginTop: 10,
2845
+ padding: '10px 12px',
2846
+ borderRadius: 6,
2847
+ border: '1px solid var(--dsw-alias-border-l1)',
2848
+ background: 'var(--dsw-alias-bg-hover)',
2849
+ },
2850
+ },
2851
+ loadingTpls
2852
+ ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading templates…')
2853
+ : React.createElement(
2854
+ 'div',
2855
+ null,
2856
+ React.createElement(
2857
+ 'div',
2858
+ { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 } },
2859
+ backendTemplates.map((bt) =>
2860
+ React.createElement(
2861
+ 'button',
2862
+ {
2863
+ key: bt.id,
2864
+ type: 'button',
2865
+ className: `dsh-goal-chip-btn ${selectedTpl?.id === bt.id ? 'active' : ''}`,
2866
+ style: selectedTpl?.id === bt.id ? { fontWeight: 600, border: '1px solid var(--dsw-alias-accent-primary)' } : {},
2867
+ onClick: () => {
2868
+ setSelectedTpl(bt);
2869
+ const initial = {};
2870
+ (bt.inputs || []).forEach((inp) => {
2871
+ initial[inp.key] = inp.defaultValue || '';
2872
+ });
2873
+ setTplInputs(initial);
2874
+ },
2875
+ },
2876
+ bt.title
2877
+ )
2878
+ )
2879
+ ),
2880
+ selectedTpl
2881
+ ? React.createElement(
2882
+ 'div',
2883
+ { style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: 8 } },
2884
+ React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: 6 } }, selectedTpl.description),
2885
+ (selectedTpl.inputs || []).map((inp) =>
2886
+ React.createElement(
2887
+ 'div',
2888
+ { key: inp.key, style: { marginBottom: 6 } },
2889
+ React.createElement('label', { style: { fontSize: 11, display: 'block', marginBottom: 2 } }, inp.label),
2890
+ React.createElement('input', {
2891
+ type: 'text',
2892
+ className: 'dsh-goal-card-input',
2893
+ style: { width: '100%', boxSizing: 'border-box', fontSize: 12, padding: '4px 8px' },
2894
+ placeholder: inp.placeholder || '',
2895
+ value: tplInputs[inp.key] || '',
2896
+ onChange: (e) => setTplInputs({ ...tplInputs, [inp.key]: e.target.value }),
2897
+ })
2898
+ )
2899
+ ),
2900
+ React.createElement(
2901
+ 'button',
2902
+ {
2903
+ type: 'button',
2904
+ className: 'dsh-goal-btn dsh-goal-btn-primary',
2905
+ style: { marginTop: 4, padding: '4px 10px', fontSize: 12 },
2906
+ onClick: handleApplyTemplate,
2907
+ },
2908
+ 'Apply Template'
2909
+ )
2910
+ )
2911
+ : null
2912
+ )
2913
+ )
2914
+ : null
2787
2915
  ),
2788
2916
 
2789
2917
  React.createElement(
@@ -3398,6 +3526,17 @@ window.__ModuleLoader__.load({
3398
3526
 
3399
3527
  const isCompleted = state.state === 'COMPLETED';
3400
3528
 
3529
+ const formatElapsed = (totalSeconds) => {
3530
+ const s = Math.max(0, Math.floor(Number(totalSeconds) || 0));
3531
+ if (s < 60) return `${s}s`;
3532
+ const m = Math.floor(s / 60);
3533
+ const remS = s % 60;
3534
+ if (m < 60) return remS > 0 ? `${m}m ${remS}s` : `${m}m`;
3535
+ const h = Math.floor(m / 60);
3536
+ const remM = m % 60;
3537
+ return remM > 0 ? `${h}h ${remM}m` : `${h}h`;
3538
+ };
3539
+
3401
3540
  const formatLiveElapsed = () => {
3402
3541
 
3403
3542
  if (!state) return '0s';
@@ -3821,7 +3960,7 @@ window.__ModuleLoader__.load({
3821
3960
  const [updateState, setUpdateState] = useState({
3822
3961
  checking: false,
3823
3962
  updating: false,
3824
- currentVersion: '0.2.3',
3963
+ currentVersion: '',
3825
3964
  latestVersion: '',
3826
3965
  updateAvailable: false,
3827
3966
  notice: '',
@@ -4159,7 +4298,7 @@ window.__ModuleLoader__.load({
4159
4298
  'div',
4160
4299
  { style: { display: 'flex', alignItems: 'center', gap: 8 } },
4161
4300
  React.createElement('span', { style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } }, t('version') || 'Version'),
4162
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'v' + (updateState.currentVersion || '0.2.3')),
4301
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, updateState.currentVersion ? 'v' + updateState.currentVersion : 'v0.2.12'),
4163
4302
  updateState.updateAvailable
4164
4303
  ? React.createElement('span', { className: 'dsh-goal-badge dsh-goal-badge-warn', style: { fontSize: 11 } }, 'v' + updateState.latestVersion + ' ' + (t('updateAvailable') || 'available'))
4165
4304
  : React.createElement('span', { className: 'dsh-goal-badge dsh-goal-badge-ok', style: { fontSize: 11 } }, '✓ ' + (t('upToDate') || 'Up to date')),
@@ -1,4 +1,8 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
1
4
  import { execFileSync } from 'node:child_process';
5
+ import { generateRetrospectiveData } from './engine-retrospective.js';
2
6
 
3
7
  /**
4
8
  * Safely get current short git commit hash
@@ -19,48 +23,119 @@ export function getGitCurrentCommit(cwd) {
19
23
  }
20
24
 
21
25
  /**
22
- * Generate Markdown report for goal results
23
- * @param {Object} state
26
+ * Format elapsed time in human readable format
27
+ * @param {number} seconds
24
28
  * @returns {string}
25
29
  */
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}\`` : '';
30
+ export function formatElapsedSeconds(seconds) {
31
+ const s = Math.max(0, Math.floor(seconds || 0));
32
+ if (s < 60) return `${s}s`;
33
+ const m = Math.floor(s / 60);
34
+ const remSec = s % 60;
35
+ if (m < 60) return `${m}m ${remSec}s`;
36
+ const h = Math.floor(m / 60);
37
+ const remMin = m % 60;
38
+ return `${h}h ${remMin}m`;
39
+ }
40
+
41
+ /**
42
+ * Generate Markdown report for goal completion or archival
43
+ * @param {object} goal - core goal view or state
44
+ * @param {object} [addonData] - { milestones, snapshots, tokensUsage }
45
+ * @param {string} [cwd]
46
+ * @returns {string}
47
+ */
48
+ export function exportReportMarkdown(goal, addonData = {}, cwd = process.cwd()) {
49
+ if (!goal) return '';
50
+ const title = goal.objective || goal.title || 'Goal Report';
51
+ const phase = goal.phase || goal.state || 'COMPLETED';
52
+ const rounds = goal.roundsStarted || goal.iterationsCount || 0;
53
+ const maxRounds = goal.maxGoalRounds || goal.maxIterations || 25;
54
+
55
+ const elapsed = goal.formattedElapsed || formatElapsedSeconds(Math.max(0, Math.round(((goal.updatedAt || goal.completedAt || Date.now()) - (goal.createdAt || goal.startedAt || Date.now())) / 1000)));
56
+
57
+ const totalTokens = addonData?.tokensUsage?.totalTokens || goal?.tokensUsage?.totalTokens || 0;
58
+ const promptTokens = addonData?.tokensUsage?.promptTokens || goal?.tokensUsage?.promptTokens || 0;
59
+ const compTokens = addonData?.tokensUsage?.completionTokens || goal?.tokensUsage?.completionTokens || 0;
60
+ const gitCommit = goal.gitStartCommit ? ` | **Git Start:** \`${goal.gitStartCommit}\`` : '';
36
61
 
37
62
  let md = `# 🎯 Goal Report: ${title}\n\n`;
38
- md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`${gitCommit}\n`;
63
+ md += `**Status:** \`${phase}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${rounds}/${maxRounds}\`${gitCommit}\n`;
39
64
  if (totalTokens > 0) {
40
65
  md += `**Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n`;
41
66
  }
42
67
  md += '\n';
43
68
 
44
- if (state.description) {
45
- md += `### Description\n${state.description}\n\n`;
69
+ if (goal.description) {
70
+ md += `### Description\n${goal.description}\n\n`;
46
71
  }
47
72
 
48
- if (state.resultSummary) {
49
- md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
73
+ if (goal.resultSummary) {
74
+ md += `### Summary & Deliverables\n${goal.resultSummary}\n\n`;
50
75
  }
51
76
 
52
- const milestones = state.milestones || [];
77
+ // Milestones & Checklists
78
+ const milestones = addonData?.milestones || goal?.milestones || [];
53
79
  if (milestones.length > 0) {
54
- md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
80
+ md += `### Milestones & Checklists\n`;
81
+ md += `| # | Status | Title | Notes | Checklists |\n`;
82
+ md += `|---|---|---|---|---|\n`;
55
83
  milestones.forEach((m, idx) => {
56
- const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
84
+ const isDone = m.status === 'completed' || m.completed === true;
85
+ const isInProg = m.status === 'in_progress';
86
+ const mark = isDone ? '✅ Done' : isInProg ? '🔄 In Progress' : '⏳ Pending';
57
87
  const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
58
- const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
59
- md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
88
+ const cleanNotes = (m.notes || '—').replace(/\|/g, '\\|');
89
+ let clSummary = '—';
90
+ if (Array.isArray(m.checklist) && m.checklist.length > 0) {
91
+ const doneCount = m.checklist.filter(c => c.done).length;
92
+ clSummary = `${doneCount}/${m.checklist.length}`;
93
+ }
94
+ md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes} | ${clSummary} |\n`;
60
95
  });
61
96
  md += '\n';
62
97
  }
63
98
 
99
+ // Snapshots
100
+ const snapshots = addonData?.snapshots || [];
101
+ if (snapshots.length > 0) {
102
+ md += `### Git Checkpoints & Snapshots\n`;
103
+ md += `| Milestone | Commit | Tag | Timestamp |\n`;
104
+ md += `|---|---|---|---|\n`;
105
+ snapshots.forEach((s) => {
106
+ const dt = new Date(s.timestamp || Date.now()).toISOString();
107
+ md += `| \`${s.milestoneId}\` | \`${s.commit}\` | \`${s.tag || '—'}\` | ${dt} |\n`;
108
+ });
109
+ md += '\n';
110
+ }
111
+
112
+ // Retrospective
113
+ try {
114
+ const retro = generateRetrospectiveData({
115
+ ...goal,
116
+ milestones,
117
+ tokensUsage: { totalTokens, promptTokens, completionTokens },
118
+ startedAt: goal.createdAt || goal.startedAt || Date.now(),
119
+ completedAt: goal.updatedAt || goal.completedAt || Date.now(),
120
+ }, cwd);
121
+ if (retro) {
122
+ md += `### Retrospective & Execution Analytics\n`;
123
+ if (retro.estimatedCostUsd) {
124
+ md += `- **Estimated Cost:** ~\$${retro.estimatedCostUsd} USD\n`;
125
+ }
126
+ if (retro.filesChangedCount !== undefined) {
127
+ md += `- **Files Changed:** ${retro.filesChangedCount}\n`;
128
+ }
129
+ if (retro.recommendations?.length > 0) {
130
+ md += `\n**Recommendations:**\n`;
131
+ retro.recommendations.forEach(r => { md += `- ${r}\n`; });
132
+ }
133
+ md += '\n';
134
+ }
135
+ } catch (err) {
136
+ // Retrospective non-fatal
137
+ }
138
+
64
139
  md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
65
140
  return md;
66
141
  }
@@ -72,10 +147,10 @@ export function exportReportMarkdown(state) {
72
147
  */
73
148
  export function exportReportGitHubPR(state) {
74
149
  if (!state) return '';
75
- const title = state.title || 'Goal Report';
76
- const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
150
+ const title = state.objective || state.title || 'Goal Report';
151
+ const status = state.phase || state.state || 'COMPLETED';
77
152
  const elapsed = state.formattedElapsed || '0s';
78
- const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
153
+ const iter = `${state.roundsStarted || state.iterationsCount || 0}/${state.maxGoalRounds || state.maxIterations || 25}`;
79
154
  const totalTokens = state.tokensUsage?.totalTokens || 0;
80
155
  const promptTokens = state.tokensUsage?.promptTokens || 0;
81
156
  const compTokens = state.tokensUsage?.completionTokens || 0;
@@ -97,11 +172,11 @@ export function exportReportGitHubPR(state) {
97
172
 
98
173
  const milestones = state.milestones || [];
99
174
  if (milestones.length > 0) {
100
- const completedCount = milestones.filter((m) => m.status === 'completed').length;
175
+ const completedCount = milestones.filter((m) => m.status === 'completed' || m.completed === true).length;
101
176
  md += `<details>\n<summary><b>📋 Milestones Breakdown (${completedCount}/${milestones.length} Completed)</b></summary>\n\n`;
102
177
  md += `| # | Status | Milestone | Notes |\n|---|---|---|---|\n`;
103
178
  milestones.forEach((m, idx) => {
104
- const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
179
+ const mark = (m.status === 'completed' || m.completed === true) ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
105
180
  const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
106
181
  const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
107
182
  md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
@@ -113,9 +188,6 @@ export function exportReportGitHubPR(state) {
113
188
  return md;
114
189
  }
115
190
 
116
- import fs from 'node:fs';
117
- import path from 'node:path';
118
-
119
191
  /**
120
192
  * Safely create a git milestone checkpoint
121
193
  * @param {object} milestone
@@ -133,7 +205,6 @@ export function createMilestoneCheckpoint(milestone, sessionId = 'default', cwd)
133
205
  cwd: cwd || undefined,
134
206
  }).trim();
135
207
 
136
- // If there are changes, auto-commit checkpoint
137
208
  if (status) {
138
209
  execFileSync('git', ['add', '-A'], {
139
210
  stdio: ['ignore', 'ignore', 'ignore'],
@@ -191,7 +262,7 @@ export function rollbackToCheckpoint(commitHash, cwd) {
191
262
  * @returns {{ relativePath: string, fullPath: string, content: string }|null}
192
263
  */
193
264
  export function saveGoalArtifact(state, cwd) {
194
- if (!state || !state.title) return null;
265
+ if (!state || (!state.title && !state.objective)) return null;
195
266
  try {
196
267
  const root = cwd || process.cwd();
197
268
  const targetDir = path.join(root, '.dsh', 'goals');
@@ -200,7 +271,7 @@ export function saveGoalArtifact(state, cwd) {
200
271
  }
201
272
 
202
273
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
203
- const safeTitle = (state.title || 'goal')
274
+ const safeTitle = (state.objective || state.title || 'goal')
204
275
  .toLowerCase()
205
276
  .replace(/[^a-z0-9]+/g, '-')
206
277
  .replace(/^-+|-+$/g, '')
@@ -218,3 +289,11 @@ export function saveGoalArtifact(state, cwd) {
218
289
  return null;
219
290
  }
220
291
  }
292
+
293
+ /**
294
+ * Save goal report to disk
295
+ */
296
+ export function saveGoalReportFile(goal, addonData = {}, baseDir = null) {
297
+ const root = baseDir || process.cwd();
298
+ return saveGoalArtifact(goal, root)?.fullPath || null;
299
+ }
@@ -6,9 +6,10 @@ import { GoalState } from './goal-engine-constants.js';
6
6
  * Storage manager for goal engine disk state persistence
7
7
  */
8
8
  export class EngineStore {
9
- constructor(engine, storagePath) {
9
+ constructor(engine, storagePath, logger) {
10
10
  this.engine = engine;
11
11
  this.storagePath = storagePath || null;
12
+ this.logger = logger || engine?.logger || console;
12
13
  this.saveTimer = null;
13
14
  }
14
15
 
@@ -77,7 +78,7 @@ export class EngineStore {
77
78
  }
78
79
  } catch (err) {
79
80
  // best-effort load failure logged defensively
80
- console.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
81
+ this.logger?.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
81
82
  }
82
83
  }
83
84
 
@@ -128,7 +129,7 @@ export class EngineStore {
128
129
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
129
130
  fs.renameSync(tmp, this.storagePath);
130
131
  } catch (err) {
131
- console.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
132
+ this.logger?.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
132
133
  }
133
134
  }
134
135