@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.
- package/lib/addon-store.js +213 -0
- package/lib/client.js +142 -3
- package/lib/engine-reports.js +112 -33
- package/lib/engine-store.js +4 -3
- package/lib/git-snapshot-manager.js +99 -0
- package/lib/goal-engine.js +10 -3
- package/lib/index.js +215 -394
- package/lib/milestone-manager.js +141 -0
- package/lib/routes.js +278 -137
- package/lib/token-budget-guard.js +128 -0
- package/lib/tools.js +115 -257
- package/package.json +4 -3
|
@@ -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: '
|
|
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' +
|
|
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')),
|
package/lib/engine-reports.js
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
* @param {
|
|
26
|
+
* Format elapsed time in human readable format
|
|
27
|
+
* @param {number} seconds
|
|
24
28
|
* @returns {string}
|
|
25
29
|
*/
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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:** \`${
|
|
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 (
|
|
45
|
-
md += `### Description\n${
|
|
69
|
+
if (goal.description) {
|
|
70
|
+
md += `### Description\n${goal.description}\n\n`;
|
|
46
71
|
}
|
|
47
72
|
|
|
48
|
-
if (
|
|
49
|
-
md += `### Summary & Deliverables\n${
|
|
73
|
+
if (goal.resultSummary) {
|
|
74
|
+
md += `### Summary & Deliverables\n${goal.resultSummary}\n\n`;
|
|
50
75
|
}
|
|
51
76
|
|
|
52
|
-
|
|
77
|
+
// Milestones & Checklists
|
|
78
|
+
const milestones = addonData?.milestones || goal?.milestones || [];
|
|
53
79
|
if (milestones.length > 0) {
|
|
54
|
-
md += `### Milestones
|
|
80
|
+
md += `### Milestones & Checklists\n`;
|
|
81
|
+
md += `| # | Status | Title | Notes | Checklists |\n`;
|
|
82
|
+
md += `|---|---|---|---|---|\n`;
|
|
55
83
|
milestones.forEach((m, idx) => {
|
|
56
|
-
const
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/lib/engine-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
132
|
+
this.logger?.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|