@goodandready/dsh-goal 0.2.12 → 0.2.14
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/engine-reports.js +112 -33
- package/lib/git-snapshot-manager.js +99 -0
- package/lib/index.js +226 -395
- package/lib/milestone-manager.js +141 -0
- package/lib/routes.js +249 -138
- package/lib/token-budget-guard.js +128 -0
- package/lib/tools.js +115 -257
- package/package.json +9 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { syncMilestoneWithIssue } from './engine-issue-sync.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Milestone and checklist coordinator for active core goals
|
|
5
|
+
*/
|
|
6
|
+
export class MilestoneManager {
|
|
7
|
+
constructor(addonStore, { gitSnapshotManager = null, enforceDependencies = true, logger = console } = {}) {
|
|
8
|
+
this.store = addonStore;
|
|
9
|
+
this.gitSnapshotManager = gitSnapshotManager;
|
|
10
|
+
this.enforceDependencies = enforceDependencies;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getMilestones(goalId) {
|
|
15
|
+
if (!goalId || !this.store) return [];
|
|
16
|
+
return this.store.getMilestones(goalId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setMilestones(goalId, milestones) {
|
|
20
|
+
if (!goalId || !this.store) return [];
|
|
21
|
+
const sanitized = (milestones || []).map((m, idx) => ({
|
|
22
|
+
id: m.id || `m-${idx + 1}`,
|
|
23
|
+
title: m.title || `Milestone ${idx + 1}`,
|
|
24
|
+
completed: Boolean(m.completed),
|
|
25
|
+
completedAt: m.completedAt || null,
|
|
26
|
+
notes: m.notes || '',
|
|
27
|
+
dependsOn: Array.isArray(m.dependsOn) ? m.dependsOn : [],
|
|
28
|
+
checklist: Array.isArray(m.checklist) ? m.checklist.map((c, cIdx) => ({
|
|
29
|
+
id: c.id || `cl-${idx + 1}-${cIdx + 1}`,
|
|
30
|
+
text: typeof c === 'string' ? c : (c.text || ''),
|
|
31
|
+
done: Boolean(c.done),
|
|
32
|
+
})) : [],
|
|
33
|
+
}));
|
|
34
|
+
this.store.setMilestones(goalId, sanitized);
|
|
35
|
+
return sanitized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
addMilestone(goalId, { title, checklist = [], dependsOn = [] }) {
|
|
39
|
+
if (!goalId || !this.store) throw new Error('Missing goalId or store');
|
|
40
|
+
const list = this.getMilestones(goalId);
|
|
41
|
+
const id = `m-${list.length + 1}`;
|
|
42
|
+
const newM = {
|
|
43
|
+
id,
|
|
44
|
+
title: title || `Milestone ${list.length + 1}`,
|
|
45
|
+
completed: false,
|
|
46
|
+
completedAt: null,
|
|
47
|
+
notes: '',
|
|
48
|
+
dependsOn: Array.isArray(dependsOn) ? dependsOn : [],
|
|
49
|
+
checklist: (checklist || []).map((c, cIdx) => ({
|
|
50
|
+
id: `cl-${list.length + 1}-${cIdx + 1}`,
|
|
51
|
+
text: typeof c === 'string' ? c : (c.text || ''),
|
|
52
|
+
done: Boolean(c.done),
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
list.push(newM);
|
|
56
|
+
this.store.setMilestones(goalId, list);
|
|
57
|
+
return newM;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
completeMilestone(goalId, milestoneId, notes = '', workspaceRoot = null) {
|
|
61
|
+
if (!goalId || !milestoneId) throw new Error('goalId and milestoneId are required');
|
|
62
|
+
const list = this.getMilestones(goalId);
|
|
63
|
+
const m = list.find(item => item.id === milestoneId);
|
|
64
|
+
if (!m) throw new Error(`Milestone "${milestoneId}" not found in goal "${goalId}"`);
|
|
65
|
+
|
|
66
|
+
// Dependency check
|
|
67
|
+
if (this.enforceDependencies && Array.isArray(m.dependsOn) && m.dependsOn.length > 0) {
|
|
68
|
+
const incomplete = m.dependsOn.filter(depId => {
|
|
69
|
+
const dep = list.find(x => x.id === depId);
|
|
70
|
+
return !dep || !dep.completed;
|
|
71
|
+
});
|
|
72
|
+
if (incomplete.length > 0) {
|
|
73
|
+
throw new Error(`Cannot complete milestone "${milestoneId}": prerequisites [${incomplete.join(', ')}] not completed`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
m.completed = true;
|
|
78
|
+
m.completedAt = Date.now();
|
|
79
|
+
if (notes) m.notes = notes;
|
|
80
|
+
if (Array.isArray(m.checklist)) {
|
|
81
|
+
m.checklist.forEach(item => { item.done = true; });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.store.setMilestones(goalId, list);
|
|
85
|
+
|
|
86
|
+
// Git snapshot if configured
|
|
87
|
+
let snapshot = null;
|
|
88
|
+
if (this.gitSnapshotManager && workspaceRoot) {
|
|
89
|
+
try {
|
|
90
|
+
snapshot = this.gitSnapshotManager.createSnapshot(goalId, milestoneId, workspaceRoot, notes);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.logger.warn?.('[MilestoneManager] Git snapshot failed:', err.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Best-effort Gitea issue checklist sync if issue data exists
|
|
97
|
+
const goalData = this.store.getGoalData(goalId);
|
|
98
|
+
if (goalData?.issue?.number) {
|
|
99
|
+
try {
|
|
100
|
+
syncMilestoneWithIssue(goalData.issue.number, m.title, true);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// sync error non-fatal
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { milestone: m, snapshot };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
toggleChecklist(goalId, milestoneId, index, done) {
|
|
110
|
+
if (!goalId || !milestoneId) throw new Error('goalId and milestoneId are required');
|
|
111
|
+
const list = this.getMilestones(goalId);
|
|
112
|
+
const m = list.find(item => item.id === milestoneId);
|
|
113
|
+
if (!m) throw new Error(`Milestone "${milestoneId}" not found in goal "${goalId}"`);
|
|
114
|
+
if (!m.checklist || index < 0 || index >= m.checklist.length) {
|
|
115
|
+
throw new Error(`Checklist item index ${index} out of bounds`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
m.checklist[index].done = typeof done === 'boolean' ? done : !m.checklist[index].done;
|
|
119
|
+
this.store.setMilestones(goalId, list);
|
|
120
|
+
return m.checklist[index];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getActiveMilestonePrompt(goalId) {
|
|
124
|
+
if (!goalId) return null;
|
|
125
|
+
const list = this.getMilestones(goalId);
|
|
126
|
+
if (list.length === 0) return null;
|
|
127
|
+
|
|
128
|
+
const active = list.find(m => !m.completed);
|
|
129
|
+
if (!active) {
|
|
130
|
+
return '[Goal Milestones]: All defined milestones are completed ✅';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const uncompletedChecklist = (active.checklist || []).filter(c => !c.done);
|
|
134
|
+
let clText = '';
|
|
135
|
+
if (active.checklist?.length > 0) {
|
|
136
|
+
clText = ` | Next items: ${active.checklist.map(c => `[${c.done ? 'x' : ' '}] ${c.text}`).join(', ')}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `[Goal Milestones]: Current step: "${active.title}"${clText}`;
|
|
140
|
+
}
|
|
141
|
+
}
|