@yemi33/minions 0.1.79 → 0.1.81

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.81 (2026-03-31)
4
+
5
+ ### Dashboard
6
+ - dashboard.js
7
+ - dashboard/js/render-schedules.js
8
+
9
+ ## 0.1.80 (2026-03-31)
10
+
11
+ ### Engine
12
+ - engine/queries.js
13
+
14
+ ### Dashboard
15
+ - dashboard.js
16
+ - dashboard/js/modal-qa.js
17
+ - dashboard/js/render-plans.js
18
+
3
19
  ## 0.1.79 (2026-03-31)
4
20
 
5
21
  ### Dashboard
@@ -218,6 +218,20 @@ async function _processQaMessage(message, selection) {
218
218
  document.getElementById('modal-body').textContent = display;
219
219
  _modalDocContext.content = display;
220
220
  }
221
+
222
+ // If editing paused an active PRD, show re-execute actions
223
+ if (data.pausedPrd && capturedFilePath) {
224
+ const planFile = capturedFilePath.replace(/^plans\//, '');
225
+ const esc = planFile.replace(/'/g, "\\'");
226
+ const actionDiv = document.createElement('div');
227
+ actionDiv.style.cssText = 'margin:8px 0;padding:8px 12px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:6px;display:flex;flex-wrap:wrap;align-items:center;gap:8px';
228
+ actionDiv.innerHTML =
229
+ '<span style="color:var(--orange);font-weight:600;font-size:12px;width:100%">Execution paused — plan was updated</span>' +
230
+ '<button onclick="qaReplacePrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Re-execute with new PRD</button>' +
231
+ '<button onclick="this.closest(\'div\').innerHTML=\'<span style=color:var(--muted);font-size:11px>Paused. No work dispatched.</span>\'" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Keep paused</button>' +
232
+ '<span style="color:var(--muted);font-size:10px;width:100%">Re-execute replaces the old PRD with a fresh one from the updated plan.</span>';
233
+ thread.appendChild(actionDiv);
234
+ }
221
235
  } else {
222
236
  const qaElapsedErr = Math.round((Date.now() - qaStartTime) / 1000);
223
237
  thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(data.error || 'Failed') + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedErr + 's</div></div>';
@@ -215,13 +215,13 @@ function renderPlans(plans) {
215
215
  const showResume = (effectiveStatus === 'paused' || effectiveStatus === 'awaiting-approval') && prdFile && !isArchived;
216
216
  const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived;
217
217
  const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow)" ' +
218
- 'onclick="event.stopPropagation();planPause(\'' + escHtml(prdFile) + '\')">Pause</button>' : '';
218
+ 'onclick="event.stopPropagation();planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>' : '';
219
219
  const resumeBtn = showResume
220
220
  ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green)" ' +
221
- 'onclick="event.stopPropagation();planApprove(\'' + escHtml(prdFile) + '\')">' + (effectiveStatus === 'awaiting-approval' ? 'Approve' : 'Resume') + '</button>'
221
+ 'onclick="event.stopPropagation();planApprove(\'' + escHtml(prdFile) + '\',this)">' + (effectiveStatus === 'awaiting-approval' ? 'Approve' : 'Resume') + '</button>'
222
222
  : '';
223
223
  const verifyBtn = showVerify ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green)" ' +
224
- 'onclick="event.stopPropagation();triggerVerify(\'' + escHtml(prdFile) + '\')">Verify</button>' : '';
224
+ 'onclick="event.stopPropagation();triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>' : '';
225
225
  const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" ' +
226
226
  'onclick="event.stopPropagation();planDelete(\'' + escHtml(p.file) + '\')">Delete</button>' : '';
227
227
 
@@ -448,11 +448,11 @@ async function planView(file) {
448
448
  const modalInProgressLabel = hasActiveWork ? '<span style="font-size:10px;color:var(--blue)">In Progress</span>' : '';
449
449
  const isModalCompleted = planStatus === 'completed';
450
450
  const modalPauseBtn = isActive && !isMdPlan && !isModalCompleted ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--yellow)" ' +
451
- 'onclick="planPause(\'' + escHtml(normalizedFile) + '\');closeModal()">Pause</button>' : '';
451
+ 'onclick="planPause(\'' + escHtml(normalizedFile) + '\',this)">Pause</button>' : '';
452
452
  const modalResumeBtn = isPaused ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
453
- 'onclick="planApprove(\'' + escHtml(normalizedFile) + '\');closeModal()">Resume</button>' : '';
453
+ 'onclick="planApprove(\'' + escHtml(normalizedFile) + '\',this)">Resume</button>' : '';
454
454
  const modalVerifyBtn = isModalCompleted ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
455
- 'onclick="triggerVerify(\'' + escHtml(normalizedFile) + '\')">Verify</button>' : '';
455
+ 'onclick="triggerVerify(\'' + escHtml(normalizedFile) + '\',this)">Verify</button>' : '';
456
456
 
457
457
  const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
458
458
  const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' +
@@ -474,17 +474,20 @@ async function planView(file) {
474
474
  } catch (e) { console.error(e); }
475
475
  }
476
476
 
477
- async function planApprove(file) {
477
+ async function planApprove(file, btn) {
478
+ if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Approving...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
478
479
  try {
479
480
  const res = await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
480
481
  if (res.ok) {
481
482
  showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);
482
483
  refreshPlans();
484
+ refresh();
483
485
  } else {
486
+ if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
484
487
  const d = await res.json().catch(() => ({}));
485
488
  alert('Approve failed: ' + (d.error || 'unknown'));
486
489
  }
487
- } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
490
+ } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } showToast('cmd-toast', 'Error: ' + e.message, false); }
488
491
  }
489
492
 
490
493
  async function planDelete(file) {
@@ -506,7 +509,8 @@ async function planDelete(file) {
506
509
  } catch (e) { alert('Error: ' + e.message); }
507
510
  }
508
511
 
509
- async function planPause(file) {
512
+ async function planPause(file, btn) {
513
+ if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Pausing...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
510
514
  try {
511
515
  const res = await fetch('/api/plans/pause', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
512
516
  if (res.ok) {
@@ -514,10 +518,11 @@ async function planPause(file) {
514
518
  refreshPlans();
515
519
  refresh();
516
520
  } else {
521
+ if (btn) { btn.textContent = btn.dataset.origText || 'Pause'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
517
522
  const d = await res.json().catch(() => ({}));
518
523
  alert('Pause failed: ' + (d.error || 'unknown'));
519
524
  }
520
- } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
525
+ } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Pause'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } showToast('cmd-toast', 'Error: ' + e.message, false); }
521
526
  }
522
527
 
523
528
  async function planReject(file) {
@@ -618,7 +623,8 @@ async function openVerifyGuide(file) {
618
623
  } catch (e) { alert('Failed to load guide: ' + e.message); }
619
624
  }
620
625
 
621
- async function triggerVerify(file) {
626
+ async function triggerVerify(file, btn) {
627
+ if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Verifying...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
622
628
  try {
623
629
  const res = await fetch('/api/plans/trigger-verify', {
624
630
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -626,13 +632,14 @@ async function triggerVerify(file) {
626
632
  });
627
633
  const d = await res.json();
628
634
  if (res.ok && d.ok) {
629
- closeModal();
635
+ try { closeModal(); } catch { /* may not be open */ }
630
636
  refresh();
631
637
  showToast('cmd-toast', d.verifyId ? 'Verify task ' + d.verifyId + ' created' : (d.message || 'Done'), true);
632
638
  } else {
639
+ if (btn) { btn.textContent = btn.dataset.origText || 'Verify'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
633
640
  alert('Failed: ' + (d.error || 'unknown'));
634
641
  }
635
- } catch (e) { alert('Error: ' + e.message); }
642
+ } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Verify'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Error: ' + e.message); }
636
643
  }
637
644
 
638
645
  window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
@@ -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>Cron</th><th>Type</th><th>Project</th><th>Agent</th><th>Enabled</th><th>Last Run</th><th></th></tr></thead><tbody>';
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><code style="font-size:10px;color:var(--blue)">' + escHtml(s.cron || '') + '</code></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
- (isEdit ? '' :
51
- '<label style="color:var(--text);font-size:var(--text-md)">ID (unique slug)' +
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
- '</label>' +
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 = document.getElementById('sched-edit-cron').value.trim();
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
- if (!id) { alert('ID is required'); return; }
116
- if (!title) { alert('Title is required'); return; }
117
- if (!cron) { alert('Cron expression is required'); return; }
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
- alert((isEdit ? 'Update' : 'Create') + ' failed: ' + (d.error || 'unknown'));
373
+ _showScheduleError((isEdit ? 'Update' : 'Create') + ' failed: ' + (d.error || 'unknown'));
129
374
  }
130
- } catch (e) { alert('Error: ' + e.message); }
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
- alert('Toggle failed: ' + (d.error || 'unknown'));
386
+ _showScheduleError('Toggle failed: ' + (d.error || 'unknown'));
142
387
  }
143
- } catch (e) { alert('Toggle error: ' + e.message); }
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
- alert('Delete failed: ' + (d.error || 'unknown'));
400
+ _showScheduleError('Delete failed: ' + (d.error || 'unknown'));
156
401
  }
157
- } catch (e) { alert('Delete error: ' + e.message); }
402
+ } catch (e) { _showScheduleError('Delete error: ' + e.message); }
158
403
  }
159
404
 
160
- window.MinionsSchedules = { renderSchedules, openCreateScheduleModal, openEditScheduleModal, submitSchedule, toggleScheduleEnabled, deleteSchedule };
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
@@ -2349,10 +2349,91 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2349
2349
  }
2350
2350
  }
2351
2351
  if (canEdit && fullPath) {
2352
- // Always save in-place — the engine's staleness detection handles PRD sync
2353
- // if the source plan changes while an active PRD is running.
2354
2352
  safeWrite(fullPath, content);
2355
- return jsonReply(res, 200, { ok: true, answer, edited: true, content, actions });
2353
+
2354
+ // If editing a plan .md that has an active PRD, auto-pause execution
2355
+ let pausedPrd = null;
2356
+ if (body.filePath && body.filePath.startsWith('plans/') && body.filePath.endsWith('.md')) {
2357
+ const planFile = body.filePath.replace(/^plans\//, '');
2358
+ try {
2359
+ const prdDir = path.join(MINIONS_DIR, 'prd');
2360
+ if (fs.existsSync(prdDir)) {
2361
+ for (const f of fs.readdirSync(prdDir)) {
2362
+ if (!f.endsWith('.json')) continue;
2363
+ const prd = safeJson(path.join(prdDir, f));
2364
+ if (!prd || prd.source_plan !== planFile) continue;
2365
+ if (prd.status === 'paused' || prd.status === 'rejected') continue;
2366
+ // Found an active PRD linked to this plan — pause it
2367
+ prd.status = 'paused';
2368
+ prd.pausedAt = new Date().toISOString();
2369
+ prd.pausedBy = 'plan-steering';
2370
+ safeWrite(path.join(prdDir, f), prd);
2371
+ pausedPrd = f;
2372
+ // Pause work items (reuse pause logic inline)
2373
+ const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
2374
+ for (const proj of PROJECTS) wiPaths.push(shared.projectWorkItemsPath(proj));
2375
+ const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
2376
+ const dispatch = JSON.parse(safeRead(dispatchPath) || '{}');
2377
+ const killedAgents = new Set();
2378
+ const resetItemIds = new Set();
2379
+ for (const wiPath of wiPaths) {
2380
+ try {
2381
+ const items = safeJson(wiPath);
2382
+ if (!items) continue;
2383
+ let changed = false;
2384
+ for (const w of items) {
2385
+ if (w.sourcePlan !== f) continue;
2386
+ if (w.status === 'done' || w.status === 'implemented' || w.status === 'complete' || w.status === 'in-pr') continue;
2387
+ if (w.status === 'dispatched') {
2388
+ const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
2389
+ if (activeEntry) {
2390
+ const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
2391
+ try {
2392
+ const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
2393
+ if (agentStatus.pid) {
2394
+ if (process.platform === 'win32') {
2395
+ try { require('child_process').execSync('taskkill /PID ' + agentStatus.pid + ' /F /T', { stdio: 'pipe', timeout: 5000 }); } catch { /* process may be dead */ }
2396
+ } else {
2397
+ try { process.kill(agentStatus.pid, 'SIGTERM'); } catch { /* process may be dead */ }
2398
+ }
2399
+ }
2400
+ agentStatus.status = 'idle';
2401
+ delete agentStatus.currentTask;
2402
+ delete agentStatus.dispatched;
2403
+ safeWrite(statusPath, agentStatus);
2404
+ } catch { /* agent reset */ }
2405
+ killedAgents.add(activeEntry.agent);
2406
+ }
2407
+ }
2408
+ w.status = 'pending';
2409
+ delete w.dispatched_at;
2410
+ delete w.dispatched_to;
2411
+ delete w.failReason;
2412
+ delete w.failedAt;
2413
+ changed = true;
2414
+ if (w.id) resetItemIds.add(w.id);
2415
+ }
2416
+ if (changed) safeWrite(wiPath, items);
2417
+ } catch { /* reset work items */ }
2418
+ }
2419
+ if (resetItemIds.size > 0 || killedAgents.size > 0) {
2420
+ mutateJsonFileLocked(dispatchPath, (dp) => {
2421
+ dp.active = (dp.active || []).filter(d => {
2422
+ if (d.meta?.item?.id && resetItemIds.has(d.meta.item.id)) return false;
2423
+ if (killedAgents.has(d.agent)) return false;
2424
+ return true;
2425
+ });
2426
+ return dp;
2427
+ }, { defaultValue: { pending: [], active: [], completed: [] } });
2428
+ }
2429
+ invalidateStatusCache();
2430
+ break;
2431
+ }
2432
+ }
2433
+ } catch (e) { console.error('auto-pause PRD on plan steer:', e.message); }
2434
+ }
2435
+
2436
+ return jsonReply(res, 200, { ok: true, answer, edited: true, content, actions, pausedPrd });
2356
2437
  }
2357
2438
  return jsonReply(res, 200, { ok: true, answer: answer + '\n\n(Read-only — changes not saved)', edited: false, actions });
2358
2439
  } catch (e) { return jsonReply(res, 500, { error: e.message }); }
@@ -2689,12 +2770,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2689
2770
 
2690
2771
  async function handleSchedulesCreate(req, res) {
2691
2772
  const body = await readBody(req);
2692
- const { id, cron, title, type, project, agent, description, priority, enabled } = body;
2693
- if (!id || !cron || !title) return jsonReply(res, 400, { error: 'id, cron, and title are required' });
2773
+ let { id, cron, title, type, project, agent, description, priority, enabled } = body;
2774
+ if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
2775
+
2776
+ // Auto-generate ID from title if not provided
2777
+ if (!id) {
2778
+ id = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
2779
+ if (!id) id = 'schedule';
2780
+ }
2694
2781
 
2695
2782
  reloadConfig();
2696
2783
  if (!CONFIG.schedules) CONFIG.schedules = [];
2697
- if (CONFIG.schedules.some(s => s.id === id)) return jsonReply(res, 400, { error: 'Schedule ID already exists' });
2784
+
2785
+ // If auto-generated ID collides, append a short numeric suffix
2786
+ if (CONFIG.schedules.some(s => s.id === id)) {
2787
+ let suffix = 2;
2788
+ while (CONFIG.schedules.some(s => s.id === `${id}-${suffix}`)) suffix++;
2789
+ id = `${id}-${suffix}`;
2790
+ }
2698
2791
 
2699
2792
  const sched = { id, cron, title, type: type || 'implement', enabled: enabled !== false };
2700
2793
  if (project) sched.project = project;
@@ -2748,6 +2841,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2748
2841
  return jsonReply(res, 200, { ok: true });
2749
2842
  }
2750
2843
 
2844
+ async function handleSchedulesParseNatural(req, res) {
2845
+ const body = await readBody(req);
2846
+ const { text } = body;
2847
+ if (!text || !text.trim()) return jsonReply(res, 400, { error: 'text is required' });
2848
+
2849
+ 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()}`;
2850
+ try {
2851
+ const result = await llm.callLLM(prompt, '', { model: 'haiku', maxTurns: 1, timeout: 30000, label: 'schedule-parse' });
2852
+ const parsed = JSON.parse(result.text.trim());
2853
+ if (!parsed.cron) return jsonReply(res, 422, { error: 'Could not parse schedule' });
2854
+ return jsonReply(res, 200, { cron: parsed.cron, description: parsed.description || '' });
2855
+ } catch (e) {
2856
+ return jsonReply(res, 422, { error: 'Parse failed: ' + e.message });
2857
+ }
2858
+ }
2859
+
2751
2860
  async function handleEngineRestart(req, res) {
2752
2861
  try {
2753
2862
  const newPid = restartEngine();
@@ -3100,8 +3209,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3100
3209
  { method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
3101
3210
 
3102
3211
  // Schedules
3212
+ { method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
3103
3213
  { method: 'GET', path: '/api/schedules', desc: 'Return schedules from config + last-run times', handler: handleSchedulesList },
3104
- { method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params: 'id, cron, title, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesCreate },
3214
+ { method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params: 'cron, title, id?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesCreate },
3105
3215
  { method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params: 'id, cron?, title?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesUpdate },
3106
3216
  { method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
3107
3217
 
package/engine/queries.js CHANGED
@@ -570,6 +570,11 @@ function getPrdInfo(config) {
570
570
  for (const wi of workItems) { if (wi.sourcePlan) wiById[wi.id] = wi; }
571
571
  } catch { /* optional */ }
572
572
  }
573
+ // Also check central work-items.json
574
+ try {
575
+ const centralWi = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
576
+ for (const wi of centralWi) { if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi; }
577
+ } catch { /* optional */ }
573
578
 
574
579
  // PR-to-PRD linking — primary source is pr-links.json (single-writer, never clobbered by polling)
575
580
  const allPrs = getPullRequests(config);
@@ -600,7 +605,9 @@ function getPrdInfo(config) {
600
605
  const statusDisplay = { dispatched: 'in-progress', pending: 'missing' };
601
606
  for (const item of items) {
602
607
  const wi = wiById[item.id];
603
- item.status = statusDisplay[item.status] || item.status || 'missing';
608
+ // Work item status is source of truth when available (PRD JSON may lag behind)
609
+ const rawStatus = wi ? (wi.status || item.status) : item.status;
610
+ item.status = statusDisplay[rawStatus] || rawStatus || 'missing';
604
611
  // Attach execution metadata for display (agent, PR link, fail reason)
605
612
  if (wi) {
606
613
  if (wi.dispatched_to) item._agent = wi.dispatched_to;
@@ -616,20 +623,15 @@ function getPrdInfo(config) {
616
623
  const missing = (byStatus['missing'] || []).length;
617
624
  const donePercent = total > 0 ? Math.round((complete / total) * 100) : 0;
618
625
 
619
- // Plan timings
626
+ // Plan timings — use wiById (already includes central work-items.json)
620
627
  const planTimings = {};
621
- for (const project of projects) {
622
- try {
623
- const workItems = safeJson(projectWorkItemsPath(project)) || [];
624
- for (const wi of workItems) {
625
- if (!wi.sourcePlan) continue;
626
- if (!planTimings[wi.sourcePlan]) planTimings[wi.sourcePlan] = { firstDispatched: null, lastCompleted: null, allDone: true };
627
- const t = planTimings[wi.sourcePlan];
628
- if (wi.dispatched_at) { const d = new Date(wi.dispatched_at).getTime(); if (!t.firstDispatched || d < t.firstDispatched) t.firstDispatched = d; }
629
- if (wi.completedAt) { const c = new Date(wi.completedAt).getTime(); if (!t.lastCompleted || c > t.lastCompleted) t.lastCompleted = c; }
630
- if (wi.status !== 'done' && wi.status !== 'in-pr') t.allDone = false; // in-pr treated as done for backward compat
631
- }
632
- } catch { /* optional */ }
628
+ for (const wi of Object.values(wiById)) {
629
+ if (!wi.sourcePlan) continue;
630
+ if (!planTimings[wi.sourcePlan]) planTimings[wi.sourcePlan] = { firstDispatched: null, lastCompleted: null, allDone: true };
631
+ const t = planTimings[wi.sourcePlan];
632
+ if (wi.dispatched_at) { const d = new Date(wi.dispatched_at).getTime(); if (!t.firstDispatched || d < t.firstDispatched) t.firstDispatched = d; }
633
+ if (wi.completedAt) { const c = new Date(wi.completedAt).getTime(); if (!t.lastCompleted || c > t.lastCompleted) t.lastCompleted = c; }
634
+ if (wi.status !== 'done' && wi.status !== 'in-pr') t.allDone = false; // in-pr treated as done for backward compat
633
635
  }
634
636
 
635
637
  const progress = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"