@yemi33/minions 0.1.80 → 0.1.82
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/CHANGELOG.md +18 -0
- package/dashboard/js/command-center.js +39 -0
- package/dashboard/js/render-schedules.js +270 -22
- package/dashboard.js +51 -5
- package/engine/lifecycle.js +14 -2
- package/package.json +1 -1
- package/playbooks/implement.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.82 (2026-03-31)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine/lifecycle.js
|
|
7
|
+
|
|
8
|
+
### Dashboard
|
|
9
|
+
- dashboard.js
|
|
10
|
+
- dashboard/js/command-center.js
|
|
11
|
+
|
|
12
|
+
### Playbooks
|
|
13
|
+
- implement.md
|
|
14
|
+
|
|
15
|
+
## 0.1.81 (2026-03-31)
|
|
16
|
+
|
|
17
|
+
### Dashboard
|
|
18
|
+
- dashboard.js
|
|
19
|
+
- dashboard/js/render-schedules.js
|
|
20
|
+
|
|
3
21
|
## 0.1.80 (2026-03-31)
|
|
4
22
|
|
|
5
23
|
### Engine
|
|
@@ -384,6 +384,45 @@ async function ccExecuteAction(action) {
|
|
|
384
384
|
}
|
|
385
385
|
break;
|
|
386
386
|
}
|
|
387
|
+
case 'schedule': {
|
|
388
|
+
const url = action._update ? '/api/schedules/update' : '/api/schedules';
|
|
389
|
+
const res = await fetch(url, {
|
|
390
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
id: action.id, title: action.title, cron: action.cron,
|
|
393
|
+
type: action.workType || 'implement',
|
|
394
|
+
project: action.project, agent: action.agent,
|
|
395
|
+
description: action.description, priority: action.priority,
|
|
396
|
+
enabled: action.enabled !== false,
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Schedule create failed'); }
|
|
400
|
+
status.innerHTML = '✓ Schedule ' + (action._update ? 'updated' : 'created') + ': <strong>' + escHtml(action.id) + '</strong>';
|
|
401
|
+
status.style.color = 'var(--green)';
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case 'delete-schedule': {
|
|
405
|
+
const res = await fetch('/api/schedules/delete', {
|
|
406
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
407
|
+
body: JSON.stringify({ id: action.id })
|
|
408
|
+
});
|
|
409
|
+
if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Schedule delete failed'); }
|
|
410
|
+
status.innerHTML = '✓ Deleted schedule: <strong>' + escHtml(action.id) + '</strong>';
|
|
411
|
+
status.style.color = 'var(--orange)';
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case 'create-meeting': {
|
|
415
|
+
const res = await fetch('/api/meetings', {
|
|
416
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
417
|
+
body: JSON.stringify({ topic: action.topic, agents: action.agents, rounds: action.rounds, project: action.project })
|
|
418
|
+
});
|
|
419
|
+
if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Meeting create failed'); }
|
|
420
|
+
const d = await res.json();
|
|
421
|
+
status.innerHTML = '✓ Meeting started: <strong>' + escHtml(action.topic) + '</strong>' + (d.id ? ' (' + escHtml(d.id) + ')' : '');
|
|
422
|
+
status.style.color = 'var(--green)';
|
|
423
|
+
wakeEngine();
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
387
426
|
default:
|
|
388
427
|
status.innerHTML = '? Unknown action: ' + escHtml(action.type);
|
|
389
428
|
status.style.color = 'var(--muted)';
|
|
@@ -1,5 +1,182 @@
|
|
|
1
1
|
// render-schedules.js — Schedule rendering functions extracted from dashboard.html
|
|
2
2
|
|
|
3
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const _DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
6
|
+
const _DAY_NAMES = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
|
|
7
|
+
|
|
8
|
+
/** Convert 3-field cron (minute hour dayOfWeek) to human-readable text */
|
|
9
|
+
function _cronToHuman(cron) {
|
|
10
|
+
if (!cron || typeof cron !== 'string') return cron || '';
|
|
11
|
+
const parts = cron.trim().split(/\s+/);
|
|
12
|
+
if (parts.length !== 3) return cron;
|
|
13
|
+
const [minute, hour, dow] = parts;
|
|
14
|
+
|
|
15
|
+
if (minute === '*' && hour === '*' && dow === '*') return 'Every minute';
|
|
16
|
+
|
|
17
|
+
const h = parseInt(hour, 10);
|
|
18
|
+
const m = parseInt(minute, 10);
|
|
19
|
+
if (isNaN(h) || isNaN(m)) return cron;
|
|
20
|
+
|
|
21
|
+
const timeStr = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
|
22
|
+
|
|
23
|
+
if (dow === '*') return 'Daily at ' + timeStr;
|
|
24
|
+
|
|
25
|
+
// Normalize comma-separated days
|
|
26
|
+
const normalized = dow.split(',').map(d => d.trim()).sort().join(',');
|
|
27
|
+
if (dow === '1-5' || normalized === '1,2,3,4,5') return 'Weekdays at ' + timeStr;
|
|
28
|
+
if (normalized === '0,6' || normalized === '6,0') return 'Weekends at ' + timeStr;
|
|
29
|
+
|
|
30
|
+
// Single day
|
|
31
|
+
const dayNum = parseInt(dow, 10);
|
|
32
|
+
if (!isNaN(dayNum) && dayNum >= 0 && dayNum <= 6 && String(dayNum) === dow) {
|
|
33
|
+
return _DAY_NAMES[dayNum] + ' at ' + timeStr;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return cron;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Parse a cron string back into picker state: { hour, minute, days } */
|
|
40
|
+
function _parseCronToPicker(cron) {
|
|
41
|
+
const result = { hour: 9, minute: 0, days: [1, 2, 3, 4, 5] }; // default weekdays 9am
|
|
42
|
+
if (!cron || typeof cron !== 'string') return result;
|
|
43
|
+
const parts = cron.trim().split(/\s+/);
|
|
44
|
+
if (parts.length !== 3) return result;
|
|
45
|
+
const [minStr, hourStr, dowStr] = parts;
|
|
46
|
+
const h = parseInt(hourStr, 10);
|
|
47
|
+
const m = parseInt(minStr, 10);
|
|
48
|
+
if (!isNaN(h)) result.hour = h;
|
|
49
|
+
if (!isNaN(m)) result.minute = m;
|
|
50
|
+
|
|
51
|
+
if (dowStr === '*') {
|
|
52
|
+
result.days = [0, 1, 2, 3, 4, 5, 6];
|
|
53
|
+
} else if (dowStr === '1-5') {
|
|
54
|
+
result.days = [1, 2, 3, 4, 5];
|
|
55
|
+
} else {
|
|
56
|
+
result.days = dowStr.split(',').map(d => parseInt(d.trim(), 10)).filter(d => !isNaN(d) && d >= 0 && d <= 6);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Build cron string from picker state */
|
|
62
|
+
function _pickerToCron(hour, minute, days) {
|
|
63
|
+
if (!days || !days.length) return '';
|
|
64
|
+
const sorted = [...days].sort((a, b) => a - b);
|
|
65
|
+
const dowStr = sorted.length === 7 ? '*'
|
|
66
|
+
: (sorted.join(',') === '1,2,3,4,5' ? '1-5' : sorted.join(','));
|
|
67
|
+
return minute + ' ' + hour + ' ' + dowStr;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Auto-generate an ID from a title */
|
|
71
|
+
function _generateScheduleId(title) {
|
|
72
|
+
const slug = (title || 'task').toLowerCase()
|
|
73
|
+
.replace(/\s+/g, '-')
|
|
74
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
75
|
+
.replace(/-+/g, '-')
|
|
76
|
+
.replace(/^-|-$/g, '')
|
|
77
|
+
.slice(0, 40);
|
|
78
|
+
const suffix = Math.random().toString(36).slice(2, 6);
|
|
79
|
+
return (slug || 'task') + '-' + suffix;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Show inline error in the schedule form */
|
|
83
|
+
function _showScheduleError(msg) {
|
|
84
|
+
const el = document.getElementById('sched-form-error');
|
|
85
|
+
if (el) {
|
|
86
|
+
el.textContent = msg;
|
|
87
|
+
el.style.display = msg ? 'block' : 'none';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Update the cron preview label from current picker state */
|
|
92
|
+
function _updateCronPreview() {
|
|
93
|
+
const hourEl = document.getElementById('sched-pick-hour');
|
|
94
|
+
const minEl = document.getElementById('sched-pick-minute');
|
|
95
|
+
if (!hourEl || !minEl) return;
|
|
96
|
+
const hour = parseInt(hourEl.value, 10);
|
|
97
|
+
const minute = parseInt(minEl.value, 10);
|
|
98
|
+
const dayBtns = document.querySelectorAll('.sched-day-pill');
|
|
99
|
+
const days = [];
|
|
100
|
+
dayBtns.forEach(btn => { if (btn.classList.contains('active')) days.push(parseInt(btn.dataset.day, 10)); });
|
|
101
|
+
const cron = _pickerToCron(hour, minute, days);
|
|
102
|
+
const previewEl = document.getElementById('sched-cron-preview');
|
|
103
|
+
if (previewEl) {
|
|
104
|
+
previewEl.textContent = cron ? '\u2192 cron: ' + cron : '(select at least one day)';
|
|
105
|
+
}
|
|
106
|
+
window._schedComputedCron = cron;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Toggle a day pill */
|
|
110
|
+
function _toggleDayPill(btn) {
|
|
111
|
+
btn.classList.toggle('active');
|
|
112
|
+
if (btn.classList.contains('active')) {
|
|
113
|
+
btn.style.background = 'var(--blue)';
|
|
114
|
+
btn.style.color = '#fff';
|
|
115
|
+
btn.style.borderColor = 'var(--blue)';
|
|
116
|
+
} else {
|
|
117
|
+
btn.style.background = 'var(--bg)';
|
|
118
|
+
btn.style.color = 'var(--text)';
|
|
119
|
+
btn.style.borderColor = 'var(--border)';
|
|
120
|
+
}
|
|
121
|
+
_updateCronPreview();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Quick-select days */
|
|
125
|
+
function _quickSelectDays(preset) {
|
|
126
|
+
const map = { all: [0,1,2,3,4,5,6], weekdays: [1,2,3,4,5], weekends: [0,6] };
|
|
127
|
+
const days = map[preset] || [];
|
|
128
|
+
document.querySelectorAll('.sched-day-pill').forEach(btn => {
|
|
129
|
+
const d = parseInt(btn.dataset.day, 10);
|
|
130
|
+
const active = days.includes(d);
|
|
131
|
+
btn.classList.toggle('active', active);
|
|
132
|
+
btn.style.background = active ? 'var(--blue)' : 'var(--bg)';
|
|
133
|
+
btn.style.color = active ? '#fff' : 'var(--text)';
|
|
134
|
+
btn.style.borderColor = active ? 'var(--blue)' : 'var(--border)';
|
|
135
|
+
});
|
|
136
|
+
_updateCronPreview();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Toggle between picker and natural language modes */
|
|
140
|
+
function _toggleCronMode() {
|
|
141
|
+
const pickerEl = document.getElementById('sched-cron-picker');
|
|
142
|
+
const nlEl = document.getElementById('sched-cron-nl');
|
|
143
|
+
const toggleLink = document.getElementById('sched-cron-mode-toggle');
|
|
144
|
+
if (!pickerEl || !nlEl) return;
|
|
145
|
+
const showingPicker = pickerEl.style.display !== 'none';
|
|
146
|
+
pickerEl.style.display = showingPicker ? 'none' : 'block';
|
|
147
|
+
nlEl.style.display = showingPicker ? 'block' : 'none';
|
|
148
|
+
if (toggleLink) toggleLink.textContent = showingPicker ? 'Use time picker' : 'Use natural language';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Parse natural language via API */
|
|
152
|
+
async function _parseNaturalCron() {
|
|
153
|
+
const textarea = document.getElementById('sched-nl-input');
|
|
154
|
+
const errEl = document.getElementById('sched-nl-error');
|
|
155
|
+
if (!textarea) return;
|
|
156
|
+
const text = textarea.value.trim();
|
|
157
|
+
if (!text) { if (errEl) errEl.textContent = 'Enter a schedule description'; return; }
|
|
158
|
+
if (errEl) errEl.textContent = '';
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch('/api/schedules/parse-natural', {
|
|
162
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ text })
|
|
164
|
+
});
|
|
165
|
+
const data = await res.json().catch(() => ({}));
|
|
166
|
+
if (res.ok && data.cron) {
|
|
167
|
+
window._schedComputedCron = data.cron;
|
|
168
|
+
const previewEl = document.getElementById('sched-cron-preview');
|
|
169
|
+
if (previewEl) previewEl.textContent = '\u2192 cron: ' + data.cron + (data.description ? ' (' + data.description + ')' : '');
|
|
170
|
+
} else {
|
|
171
|
+
if (errEl) errEl.textContent = data.error || 'Failed to parse schedule';
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (errEl) errEl.textContent = 'Network error: ' + e.message;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Rendering ──────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
3
180
|
function renderSchedules(schedules) {
|
|
4
181
|
const el = document.getElementById('scheduled-content');
|
|
5
182
|
const countEl = document.getElementById('scheduled-count');
|
|
@@ -8,17 +185,18 @@ function renderSchedules(schedules) {
|
|
|
8
185
|
el.innerHTML = '<p class="empty">No scheduled tasks. Add one to automate recurring work.</p>';
|
|
9
186
|
return;
|
|
10
187
|
}
|
|
11
|
-
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>
|
|
188
|
+
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Schedule</th><th>Type</th><th>Project</th><th>Agent</th><th>Enabled</th><th>Last Run</th><th></th></tr></thead><tbody>';
|
|
12
189
|
for (const s of schedules) {
|
|
13
190
|
const enabledBadge = s.enabled
|
|
14
191
|
? '<span class="pr-badge approved">enabled</span>'
|
|
15
192
|
: '<span class="pr-badge rejected">disabled</span>';
|
|
16
193
|
const lastRun = s._lastRun ? timeAgo(s._lastRun) : 'never';
|
|
17
194
|
const typeBadge = '<span class="dispatch-type ' + escHtml(s.type || 'implement') + '">' + escHtml(s.type || 'implement') + '</span>';
|
|
195
|
+
const humanCron = _cronToHuman(s.cron || '');
|
|
18
196
|
html += '<tr>' +
|
|
19
197
|
'<td><span class="pr-id">' + escHtml(s.id || '') + '</span></td>' +
|
|
20
198
|
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(s.title || '') + '">' + escHtml(s.title || '') + '</td>' +
|
|
21
|
-
'<td><
|
|
199
|
+
'<td><span title="' + escHtml(s.cron || '') + '" style="font-size:11px;color:var(--blue)">' + escHtml(humanCron) + '</span></td>' +
|
|
22
200
|
'<td>' + typeBadge + '</td>' +
|
|
23
201
|
'<td><span style="font-size:10px;color:var(--muted)">' + escHtml(s.project || '') + '</span></td>' +
|
|
24
202
|
'<td><span class="pr-agent">' + escHtml(s.agent || 'auto') + '</span></td>' +
|
|
@@ -36,6 +214,8 @@ function renderSchedules(schedules) {
|
|
|
36
214
|
window._lastSchedules = schedules;
|
|
37
215
|
}
|
|
38
216
|
|
|
217
|
+
// ─── Form ───────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
39
219
|
function _scheduleFormHtml(sched, isEdit) {
|
|
40
220
|
const types = ['implement', 'test', 'explore', 'ask', 'review', 'fix'];
|
|
41
221
|
const priorities = ['high', 'medium', 'low'];
|
|
@@ -45,18 +225,74 @@ function _scheduleFormHtml(sched, isEdit) {
|
|
|
45
225
|
const agentOpts = '<option value="">Auto</option>' + cmdAgents.map(a => '<option value="' + escHtml(a.id) + '"' + (sched.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
|
|
46
226
|
|
|
47
227
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
228
|
+
const pillStyle = 'display:inline-block;padding:4px 10px;margin:2px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-size:11px;color:var(--text);background:var(--bg);user-select:none;transition:all 0.15s';
|
|
229
|
+
const pillActiveExtra = 'background:var(--blue);color:#fff;border-color:var(--blue)';
|
|
230
|
+
const linkStyle = 'font-size:10px;color:var(--blue);cursor:pointer;text-decoration:underline;margin-right:8px';
|
|
231
|
+
|
|
232
|
+
// Parse existing cron for picker defaults
|
|
233
|
+
const picker = _parseCronToPicker(sched.cron || '');
|
|
234
|
+
const computedCron = sched.cron || _pickerToCron(picker.hour, picker.minute, picker.days);
|
|
235
|
+
|
|
236
|
+
// Hour options (0-23)
|
|
237
|
+
let hourOpts = '';
|
|
238
|
+
for (let h = 0; h < 24; h++) {
|
|
239
|
+
hourOpts += '<option value="' + h + '"' + (picker.hour === h ? ' selected' : '') + '>' + String(h).padStart(2, '0') + '</option>';
|
|
240
|
+
}
|
|
241
|
+
// Minute options (0, 5, 10, ..., 55)
|
|
242
|
+
let minOpts = '';
|
|
243
|
+
for (let m = 0; m <= 55; m += 5) {
|
|
244
|
+
minOpts += '<option value="' + m + '"' + (picker.minute === m ? ' selected' : '') + '>' + String(m).padStart(2, '0') + '</option>';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Day pills
|
|
248
|
+
const dayOrder = [1, 2, 3, 4, 5, 6, 0]; // Mon-Sun
|
|
249
|
+
let dayPills = '';
|
|
250
|
+
for (const d of dayOrder) {
|
|
251
|
+
const isActive = picker.days.includes(d);
|
|
252
|
+
dayPills += '<span class="sched-day-pill' + (isActive ? ' active' : '') + '" data-day="' + d + '" ' +
|
|
253
|
+
'style="' + pillStyle + (isActive ? ';' + pillActiveExtra : '') + '" ' +
|
|
254
|
+
'onclick="_toggleDayPill(this)">' + _DAYS[d] + '</span>';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ID section: auto-generated for create, read-only label for edit
|
|
258
|
+
let idSection = '';
|
|
259
|
+
if (isEdit) {
|
|
260
|
+
idSection = '<div style="color:var(--muted);font-size:11px;margin-bottom:4px">ID: <strong style="color:var(--text)">' + escHtml(sched.id || '') + '</strong></div>';
|
|
261
|
+
} else {
|
|
262
|
+
idSection = '<div id="sched-auto-id" style="color:var(--muted);font-size:11px;margin-bottom:4px">ID: <strong style="color:var(--text)">auto-generated from title</strong></div>';
|
|
263
|
+
}
|
|
48
264
|
|
|
49
265
|
return '<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'<input id="sched-edit-id" value="' + escHtml(sched.id || '') + '" placeholder="e.g. nightly-tests" style="' + inputStyle + '">' +
|
|
53
|
-
'</label>') +
|
|
266
|
+
idSection +
|
|
267
|
+
'<div id="sched-form-error" style="display:none;color:var(--red);font-size:12px;padding:6px 10px;background:rgba(255,50,50,0.1);border-radius:var(--radius-sm)"></div>' +
|
|
54
268
|
'<label style="color:var(--text);font-size:var(--text-md)">Title' +
|
|
55
|
-
'<input id="sched-edit-title" value="' + escHtml(sched.title || '') + '" style="' + inputStyle + '"
|
|
56
|
-
|
|
57
|
-
'<label style="color:var(--text);font-size:var(--text-md)">Cron <span style="font-size:10px;color:var(--muted)">(minute hour dayOfWeek)</span>' +
|
|
58
|
-
'<input id="sched-edit-cron" value="' + escHtml(sched.cron || '') + '" placeholder="0 2 *" style="' + inputStyle + '">' +
|
|
269
|
+
'<input id="sched-edit-title" value="' + escHtml(sched.title || '') + '" style="' + inputStyle + '"' +
|
|
270
|
+
(!isEdit ? " oninput=\"(function(v){var el=document.querySelector('#sched-auto-id strong');if(el)el.textContent=v?window._generateScheduleId(v):'auto-generated from title'})(this.value)\"" : '') + '>' +
|
|
59
271
|
'</label>' +
|
|
272
|
+
'<div id="sched-cron-picker" style="display:block">' +
|
|
273
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Schedule</label>' +
|
|
274
|
+
'<div style="display:flex;gap:8px;align-items:center;margin-top:4px">' +
|
|
275
|
+
'<select id="sched-pick-hour" style="' + inputStyle + ';width:auto;display:inline-block" onchange="_updateCronPreview()">' + hourOpts + '</select>' +
|
|
276
|
+
'<span style="color:var(--muted)">:</span>' +
|
|
277
|
+
'<select id="sched-pick-minute" style="' + inputStyle + ';width:auto;display:inline-block" onchange="_updateCronPreview()">' + minOpts + '</select>' +
|
|
278
|
+
'</div>' +
|
|
279
|
+
'<div style="margin-top:8px">' + dayPills + '</div>' +
|
|
280
|
+
'<div style="margin-top:6px">' +
|
|
281
|
+
'<span style="' + linkStyle + "\" onclick=\"_quickSelectDays('all')\">Every day</span>" +
|
|
282
|
+
'<span style="' + linkStyle + "\" onclick=\"_quickSelectDays('weekdays')\">Weekdays</span>" +
|
|
283
|
+
'<span style="' + linkStyle + "\" onclick=\"_quickSelectDays('weekends')\">Weekends</span>" +
|
|
284
|
+
'</div>' +
|
|
285
|
+
'</div>' +
|
|
286
|
+
'<div id="sched-cron-nl" style="display:none">' +
|
|
287
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Schedule (natural language)</label>' +
|
|
288
|
+
'<textarea id="sched-nl-input" rows="2" placeholder="every weekday at 9am" style="' + inputStyle + ';resize:vertical;margin-top:4px"></textarea>' +
|
|
289
|
+
'<button onclick="_parseNaturalCron()" style="margin-top:6px;padding:4px 12px;font-size:11px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Parse \u2192</button>' +
|
|
290
|
+
'<div id="sched-nl-error" style="color:var(--red);font-size:11px;margin-top:4px"></div>' +
|
|
291
|
+
'</div>' +
|
|
292
|
+
'<div style="margin-top:2px">' +
|
|
293
|
+
'<span id="sched-cron-preview" style="font-size:11px;color:var(--blue)">\u2192 cron: ' + escHtml(computedCron) + '</span>' +
|
|
294
|
+
'<br><span id="sched-cron-mode-toggle" style="' + linkStyle + ';margin-top:4px;display:inline-block" onclick="_toggleCronMode()">Use natural language</span>' +
|
|
295
|
+
'</div>' +
|
|
60
296
|
'<div style="display:flex;gap:12px">' +
|
|
61
297
|
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Type' +
|
|
62
298
|
'<select id="sched-edit-type" style="' + inputStyle + '">' + typeOpts + '</select>' +
|
|
@@ -84,16 +320,19 @@ function _scheduleFormHtml(sched, isEdit) {
|
|
|
84
320
|
}
|
|
85
321
|
|
|
86
322
|
function openCreateScheduleModal() {
|
|
323
|
+
window._schedComputedCron = '';
|
|
87
324
|
document.getElementById('modal-title').textContent = 'New Scheduled Task';
|
|
88
325
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
89
326
|
document.getElementById('modal-body').style.fontFamily = '';
|
|
90
327
|
document.getElementById('modal-body').innerHTML = _scheduleFormHtml({}, false);
|
|
91
328
|
document.getElementById('modal').classList.add('open');
|
|
329
|
+
_updateCronPreview();
|
|
92
330
|
}
|
|
93
331
|
|
|
94
332
|
function openEditScheduleModal(id) {
|
|
95
333
|
const sched = (window._lastSchedules || []).find(s => s.id === id);
|
|
96
334
|
if (!sched) return;
|
|
335
|
+
window._schedComputedCron = sched.cron || '';
|
|
97
336
|
document.getElementById('modal-title').textContent = 'Edit Schedule: ' + id;
|
|
98
337
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
99
338
|
document.getElementById('modal-body').style.fontFamily = '';
|
|
@@ -103,18 +342,24 @@ function openEditScheduleModal(id) {
|
|
|
103
342
|
}
|
|
104
343
|
|
|
105
344
|
async function submitSchedule(isEdit) {
|
|
345
|
+
_showScheduleError('');
|
|
106
346
|
const title = document.getElementById('sched-edit-title').value.trim();
|
|
107
|
-
const cron =
|
|
347
|
+
const cron = window._schedComputedCron || '';
|
|
108
348
|
const type = document.getElementById('sched-edit-type').value;
|
|
109
349
|
const priority = document.getElementById('sched-edit-priority').value;
|
|
110
350
|
const project = document.getElementById('sched-edit-project').value;
|
|
111
351
|
const agent = document.getElementById('sched-edit-agent').value;
|
|
112
352
|
const description = document.getElementById('sched-edit-desc').value;
|
|
113
|
-
const id = isEdit ? window._editScheduleId : (document.getElementById('sched-edit-id') ? document.getElementById('sched-edit-id').value.trim() : '');
|
|
114
353
|
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
354
|
+
let id;
|
|
355
|
+
if (isEdit) {
|
|
356
|
+
id = window._editScheduleId;
|
|
357
|
+
} else {
|
|
358
|
+
id = _generateScheduleId(title);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!title) { _showScheduleError('Title is required'); return; }
|
|
362
|
+
if (!cron) { _showScheduleError('Schedule is required \u2014 select days and time, or use natural language'); return; }
|
|
118
363
|
|
|
119
364
|
const payload = { id, title, cron, type, priority, project: project || undefined, agent: agent || undefined, description: description || undefined, enabled: true };
|
|
120
365
|
const url = isEdit ? '/api/schedules/update' : '/api/schedules';
|
|
@@ -125,9 +370,9 @@ async function submitSchedule(isEdit) {
|
|
|
125
370
|
});
|
|
126
371
|
if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', isEdit ? 'Schedule updated' : 'Schedule created', true); } else {
|
|
127
372
|
const d = await res.json().catch(() => ({}));
|
|
128
|
-
|
|
373
|
+
_showScheduleError((isEdit ? 'Update' : 'Create') + ' failed: ' + (d.error || 'unknown'));
|
|
129
374
|
}
|
|
130
|
-
} catch (e) {
|
|
375
|
+
} catch (e) { _showScheduleError('Error: ' + e.message); }
|
|
131
376
|
}
|
|
132
377
|
|
|
133
378
|
async function toggleScheduleEnabled(id, enabled) {
|
|
@@ -138,9 +383,9 @@ async function toggleScheduleEnabled(id, enabled) {
|
|
|
138
383
|
});
|
|
139
384
|
if (res.ok) { refresh(); } else {
|
|
140
385
|
const d = await res.json().catch(() => ({}));
|
|
141
|
-
|
|
386
|
+
_showScheduleError('Toggle failed: ' + (d.error || 'unknown'));
|
|
142
387
|
}
|
|
143
|
-
} catch (e) {
|
|
388
|
+
} catch (e) { _showScheduleError('Toggle error: ' + e.message); }
|
|
144
389
|
}
|
|
145
390
|
|
|
146
391
|
async function deleteSchedule(id) {
|
|
@@ -152,9 +397,12 @@ async function deleteSchedule(id) {
|
|
|
152
397
|
});
|
|
153
398
|
if (res.ok) { refresh(); showToast('cmd-toast', 'Schedule deleted', true); } else {
|
|
154
399
|
const d = await res.json().catch(() => ({}));
|
|
155
|
-
|
|
400
|
+
_showScheduleError('Delete failed: ' + (d.error || 'unknown'));
|
|
156
401
|
}
|
|
157
|
-
} catch (e) {
|
|
402
|
+
} catch (e) { _showScheduleError('Delete error: ' + e.message); }
|
|
158
403
|
}
|
|
159
404
|
|
|
160
|
-
|
|
405
|
+
// Expose _generateScheduleId globally for the inline oninput handler
|
|
406
|
+
window._generateScheduleId = _generateScheduleId;
|
|
407
|
+
|
|
408
|
+
window.MinionsSchedules = { renderSchedules, openCreateScheduleModal, openEditScheduleModal, submitSchedule, toggleScheduleEnabled, deleteSchedule, _cronToHuman, _parseNaturalCron, _toggleCronMode, _quickSelectDays, _toggleDayPill, _updateCronPreview };
|
package/dashboard.js
CHANGED
|
@@ -371,6 +371,9 @@ Available action types:
|
|
|
371
371
|
- **plan-edit**: Revise/edit a plan .md file. Fields: file (plan .md filename from plans/), instruction (what to change).
|
|
372
372
|
- **execute-plan**: Execute an existing plan .md file. Fields: file (plan .md filename), project (optional)
|
|
373
373
|
- **file-edit**: Edit any minions file via LLM. Fields: file (path relative to minions dir), instruction (what to change).
|
|
374
|
+
- **schedule**: Create or update a scheduled task. Fields: id (unique slug), title, cron (3-field: minute hour dayOfWeek), workType (implement/test/explore/ask/review/fix), project (optional), agent (optional), description (optional), priority (optional), enabled (default true). Example cron: "0 9 2" = every Tuesday at 9am.
|
|
375
|
+
- **delete-schedule**: Delete a scheduled task. Fields: id.
|
|
376
|
+
- **create-meeting**: Start a team meeting. Fields: topic, agents (array of agent IDs), rounds (optional, default 3), project (optional).
|
|
374
377
|
|
|
375
378
|
## Rules
|
|
376
379
|
|
|
@@ -419,7 +422,8 @@ ${projects}
|
|
|
419
422
|
### Scheduled Tasks
|
|
420
423
|
${schedSummary}
|
|
421
424
|
|
|
422
|
-
|
|
425
|
+
### Dashboard API (all endpoints)
|
|
426
|
+
${_getApiRoutesSummary()}
|
|
423
427
|
|
|
424
428
|
For details on any of the above, use your tools to read files under \`${MINIONS_DIR}\`.`;
|
|
425
429
|
}
|
|
@@ -446,6 +450,16 @@ function parseCCActions(text) {
|
|
|
446
450
|
return { text: displayText, actions };
|
|
447
451
|
}
|
|
448
452
|
|
|
453
|
+
// ── API routes reference for CC — populated by server setup ──────────────────
|
|
454
|
+
let _apiRoutesRef = null; // set to ROUTES array once server initializes
|
|
455
|
+
function _getApiRoutesSummary() {
|
|
456
|
+
if (!_apiRoutesRef) return '(API routes not yet loaded — fetch GET /api/routes to discover endpoints)';
|
|
457
|
+
return _apiRoutesRef
|
|
458
|
+
.filter(r => r.path !== '/api/routes' && typeof r.path === 'string')
|
|
459
|
+
.map(r => `- \`${r.method} ${r.path}\` — ${r.desc}${r.params ? ' | Params: ' + r.params : ''}`)
|
|
460
|
+
.join('\n');
|
|
461
|
+
}
|
|
462
|
+
|
|
449
463
|
// ── Shared LLM call core — used by CC panel and doc modals ──────────────────
|
|
450
464
|
|
|
451
465
|
// Session store for doc modals — keyed by filePath or title, persisted to disk
|
|
@@ -2770,12 +2784,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2770
2784
|
|
|
2771
2785
|
async function handleSchedulesCreate(req, res) {
|
|
2772
2786
|
const body = await readBody(req);
|
|
2773
|
-
|
|
2774
|
-
if (!
|
|
2787
|
+
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
2788
|
+
if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
|
|
2789
|
+
|
|
2790
|
+
// Auto-generate ID from title if not provided
|
|
2791
|
+
if (!id) {
|
|
2792
|
+
id = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
|
|
2793
|
+
if (!id) id = 'schedule';
|
|
2794
|
+
}
|
|
2775
2795
|
|
|
2776
2796
|
reloadConfig();
|
|
2777
2797
|
if (!CONFIG.schedules) CONFIG.schedules = [];
|
|
2778
|
-
|
|
2798
|
+
|
|
2799
|
+
// If auto-generated ID collides, append a short numeric suffix
|
|
2800
|
+
if (CONFIG.schedules.some(s => s.id === id)) {
|
|
2801
|
+
let suffix = 2;
|
|
2802
|
+
while (CONFIG.schedules.some(s => s.id === `${id}-${suffix}`)) suffix++;
|
|
2803
|
+
id = `${id}-${suffix}`;
|
|
2804
|
+
}
|
|
2779
2805
|
|
|
2780
2806
|
const sched = { id, cron, title, type: type || 'implement', enabled: enabled !== false };
|
|
2781
2807
|
if (project) sched.project = project;
|
|
@@ -2829,6 +2855,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2829
2855
|
return jsonReply(res, 200, { ok: true });
|
|
2830
2856
|
}
|
|
2831
2857
|
|
|
2858
|
+
async function handleSchedulesParseNatural(req, res) {
|
|
2859
|
+
const body = await readBody(req);
|
|
2860
|
+
const { text } = body;
|
|
2861
|
+
if (!text || !text.trim()) return jsonReply(res, 400, { error: 'text is required' });
|
|
2862
|
+
|
|
2863
|
+
const prompt = `Convert this schedule description to a 3-field cron expression (minute hour dayOfWeek, where dayOfWeek is 0=Sun..6=Sat or ranges like 1-5). Return JSON only: {"cron": "...", "description": "..."}. Input: ${text.trim()}`;
|
|
2864
|
+
try {
|
|
2865
|
+
const result = await llm.callLLM(prompt, '', { model: 'haiku', maxTurns: 1, timeout: 30000, label: 'schedule-parse' });
|
|
2866
|
+
const parsed = JSON.parse(result.text.trim());
|
|
2867
|
+
if (!parsed.cron) return jsonReply(res, 422, { error: 'Could not parse schedule' });
|
|
2868
|
+
return jsonReply(res, 200, { cron: parsed.cron, description: parsed.description || '' });
|
|
2869
|
+
} catch (e) {
|
|
2870
|
+
return jsonReply(res, 422, { error: 'Parse failed: ' + e.message });
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2832
2874
|
async function handleEngineRestart(req, res) {
|
|
2833
2875
|
try {
|
|
2834
2876
|
const newPid = restartEngine();
|
|
@@ -3181,8 +3223,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3181
3223
|
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
3182
3224
|
|
|
3183
3225
|
// Schedules
|
|
3226
|
+
{ method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
|
|
3184
3227
|
{ method: 'GET', path: '/api/schedules', desc: 'Return schedules from config + last-run times', handler: handleSchedulesList },
|
|
3185
|
-
{ method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params: '
|
|
3228
|
+
{ method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params: 'cron, title, id?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesCreate },
|
|
3186
3229
|
{ method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params: 'id, cron?, title?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesUpdate },
|
|
3187
3230
|
{ method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
|
|
3188
3231
|
|
|
@@ -3281,6 +3324,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3281
3324
|
{ method: 'POST', path: '/api/settings/routing', desc: 'Update routing.md', params: 'content', handler: handleSettingsRouting },
|
|
3282
3325
|
];
|
|
3283
3326
|
|
|
3327
|
+
// Expose routes to CC preamble builder (once, on first request)
|
|
3328
|
+
if (!_apiRoutesRef) _apiRoutesRef = ROUTES;
|
|
3329
|
+
|
|
3284
3330
|
// ── Route Dispatcher ────────────────────────────────────────────────────────
|
|
3285
3331
|
|
|
3286
3332
|
const pathname = req.url.split('?')[0];
|
package/engine/lifecycle.js
CHANGED
|
@@ -1136,8 +1136,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1136
1136
|
const projects = shared.getProjects(config);
|
|
1137
1137
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
1138
1138
|
if (!existingPrFound) {
|
|
1139
|
-
e.log('warn', `Agent completed implement task ${meta.item.id} but no PR was created`);
|
|
1140
|
-
//
|
|
1139
|
+
e.log('warn', `Agent completed implement task ${meta.item.id} but no PR was created — reverting to failed for retry`);
|
|
1140
|
+
// Revert to failed so auto-retry can re-attempt with PR creation
|
|
1141
1141
|
let wiPath;
|
|
1142
1142
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
1143
1143
|
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -1149,6 +1149,18 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1149
1149
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1150
1150
|
if (wi) {
|
|
1151
1151
|
wi.noPr = true;
|
|
1152
|
+
wi.failReason = 'Completed without creating a pull request';
|
|
1153
|
+
const retries = wi._retryCount || 0;
|
|
1154
|
+
if (retries < 3) {
|
|
1155
|
+
wi.status = 'pending';
|
|
1156
|
+
wi._retryCount = retries + 1;
|
|
1157
|
+
delete wi.dispatched_at;
|
|
1158
|
+
delete wi.dispatched_to;
|
|
1159
|
+
e.log('info', `Auto-retry ${retries + 1}/3 for ${meta.item.id} (no PR created)`);
|
|
1160
|
+
} else {
|
|
1161
|
+
wi.status = 'failed';
|
|
1162
|
+
e.log('warn', `${meta.item.id} failed after 3 retries — no PR created`);
|
|
1163
|
+
}
|
|
1152
1164
|
shared.safeWrite(wiPath, items);
|
|
1153
1165
|
}
|
|
1154
1166
|
}
|
package/package.json
CHANGED
package/playbooks/implement.md
CHANGED
|
@@ -56,7 +56,9 @@ cd {{team_root}}
|
|
|
56
56
|
git worktree remove ../worktrees/{{branch_name}} --force
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
## Create PR
|
|
59
|
+
## Create PR (MANDATORY)
|
|
60
|
+
|
|
61
|
+
**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.
|
|
60
62
|
|
|
61
63
|
{{pr_create_instructions}}
|
|
62
64
|
- sourceRefName: `refs/heads/{{branch_name}}`
|