@yemi33/minions 0.1.113 → 0.1.115

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.115 (2026-04-01)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/shared.js
8
+
9
+ ### Dashboard
10
+ - dashboard.js
11
+ - dashboard/js/settings.js
12
+
13
+ ## 0.1.114 (2026-04-01)
14
+
15
+ ### Dashboard
16
+ - dashboard/js/render-inbox.js
17
+ - dashboard/js/render-kb.js
18
+ - dashboard/js/render-meetings.js
19
+ - dashboard/js/render-pinned.js
20
+ - dashboard/js/render-prs.js
21
+ - dashboard/js/render-schedules.js
22
+ - dashboard/js/render-work-items.js
23
+ - dashboard/js/utils.js
24
+
3
25
  ## 0.1.113 (2026-04-01)
4
26
 
5
27
  ### Dashboard
@@ -1,6 +1,7 @@
1
1
  // render-inbox.js — Inbox and notes rendering functions extracted from dashboard.html
2
2
 
3
3
  function renderInbox(inbox) {
4
+ inbox = inbox.filter(function(item) { return !isDeleted('inbox:' + item.name); });
4
5
  inboxData = inbox;
5
6
  const list = document.getElementById('inbox-list');
6
7
  const count = document.getElementById('inbox-count');
@@ -128,13 +129,16 @@ function modalCancelEdit() {
128
129
 
129
130
  async function deleteInboxItem(name) {
130
131
  if (!confirm('Delete "' + name + '" from inbox?')) return;
132
+ markDeleted('inbox:' + name);
133
+ const card = document.querySelector('.inbox-item[data-file="notes/inbox/' + CSS.escape(name) + '"]');
134
+ if (card) card.remove();
131
135
  try {
132
136
  const res = await fetch('/api/inbox/delete', {
133
137
  method: 'POST', headers: { 'Content-Type': 'application/json' },
134
138
  body: JSON.stringify({ name })
135
139
  });
136
- if (res.ok) { refresh(); } else { const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
137
- } catch (e) { alert('Error: ' + e.message); }
140
+ if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Delete failed: ' + (d.error || 'unknown')); refresh(); }
141
+ } catch (e) { alert('Delete error: ' + e.message); refresh(); }
138
142
  }
139
143
 
140
144
  async function openInboxInExplorer(name) {
@@ -169,35 +173,33 @@ async function submitQuickNote() {
169
173
  const title = titleEl.value;
170
174
  const content = contentEl.value;
171
175
  if (!title && !content) { alert('Title or content required'); return; }
176
+ try { closeModal(); } catch { /* expected */ }
177
+ showToast('cmd-toast', 'Note saved to inbox', true);
172
178
  try {
173
179
  const res = await fetch('/api/notes', {
174
180
  method: 'POST', headers: { 'Content-Type': 'application/json' },
175
181
  body: JSON.stringify({ title: title || 'Quick note', what: content || title })
176
182
  });
177
- if (res.ok) {
178
- try { closeModal(); } catch { /* expected */ }
179
- refresh();
180
- try { showToast('cmd-toast', 'Note saved to inbox', true); } catch { /* expected */ }
181
- }
182
- else { const d = await res.json().catch(() => ({})); alert('Error: ' + (d.error || 'unknown')); }
183
- } catch (e) { alert('Error saving note: ' + e.message); }
183
+ if (res.ok) { refresh(); }
184
+ else { const d = await res.json().catch(() => ({})); alert('Note failed: ' + (d.error || 'unknown')); openQuickNoteModal(); }
185
+ } catch (e) { alert('Error saving note: ' + e.message); openQuickNoteModal(); }
184
186
  }
185
187
 
186
188
  async function doPromoteToKB(name, category) {
189
+ try { closeModal(); } catch { /* expected */ }
190
+ markDeleted('inbox:' + name);
191
+ const card = document.querySelector('.inbox-item[data-file="notes/inbox/' + CSS.escape(name) + '"]');
192
+ if (card) card.remove();
193
+ showToast('cmd-toast', 'Promoted to Knowledge Base', true);
187
194
  try {
188
195
  const res = await fetch('/api/inbox/promote-kb', {
189
196
  method: 'POST', headers: { 'Content-Type': 'application/json' },
190
197
  body: JSON.stringify({ name, category })
191
198
  });
192
199
  const data = await res.json();
193
- if (res.ok) {
194
- closeModal();
195
- refresh();
196
- refreshKnowledgeBase();
197
- } else {
198
- alert('Failed: ' + (data.error || 'unknown'));
199
- }
200
- } catch (e) { alert('Error: ' + e.message); }
200
+ if (res.ok) { refreshKnowledgeBase(); }
201
+ else { alert('Failed: ' + (data.error || 'unknown')); refresh(); }
202
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
201
203
  }
202
204
 
203
205
  window.MinionsInbox = { renderInbox, promoteToKB, renderNotes, openNotesModal, modalToggleEdit, modalSaveEdit, modalCancelEdit, deleteInboxItem, openInboxInExplorer, openQuickNoteModal, submitQuickNote, doPromoteToKB };
@@ -147,14 +147,15 @@ async function submitKbEntry() {
147
147
  const title = document.getElementById('kb-new-title').value;
148
148
  const content = document.getElementById('kb-new-content').value;
149
149
  if (!title || !content) { alert('Title and content are required'); return; }
150
+ try { closeModal(); } catch { /* may not be open */ }
151
+ showToast('cmd-toast', 'KB entry created', true);
150
152
  try {
151
153
  const res = await fetch('/api/knowledge', {
152
154
  method: 'POST', headers: { 'Content-Type': 'application/json' },
153
155
  body: JSON.stringify({ category, title, content })
154
156
  });
155
- if (res.ok) { closeModal(); refreshKnowledgeBase(); showToast('cmd-toast', 'KB entry created', true); }
156
- else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
157
- } catch (e) { alert('Error: ' + e.message); }
157
+ if (res.ok) { refreshKnowledgeBase(); } else { const d = await res.json().catch(() => ({})); alert('KB create failed: ' + (d.error || 'unknown')); openNewKbModal(); }
158
+ } catch (e) { alert('Error: ' + e.message); openNewKbModal(); }
158
159
  }
159
160
 
160
161
  async function kbOpenItem(category, file) {
@@ -1,8 +1,11 @@
1
1
  // render-meetings.js — Team meeting rendering
2
2
 
3
3
  let _showArchived = false;
4
+ const MTG_PER_PAGE = 10;
5
+ let _mtgPage = 0;
4
6
 
5
7
  function renderMeetings(meetings) {
8
+ meetings = (meetings || []).filter(function(m) { return !isDeleted('mtg:' + m.id); });
6
9
  const el = document.getElementById('meetings-content');
7
10
  const countEl = document.getElementById('meetings-count');
8
11
  if (!meetings || meetings.length === 0) {
@@ -25,7 +28,12 @@ function renderMeetings(meetings) {
25
28
  return;
26
29
  }
27
30
 
28
- el.innerHTML = visible.map(m => {
31
+ const totalPages = Math.ceil(visible.length / MTG_PER_PAGE);
32
+ if (_mtgPage >= totalPages) _mtgPage = totalPages - 1;
33
+ const start = _mtgPage * MTG_PER_PAGE;
34
+ const pageItems = visible.slice(start, start + MTG_PER_PAGE);
35
+
36
+ el.innerHTML = pageItems.map(m => {
29
37
  const statusColor = statusColors[m.status] || 'var(--muted)';
30
38
  const statusLabel = statusLabels[m.status] || m.status;
31
39
  const participantBadges = (m.participants || []).map(p => {
@@ -55,14 +63,27 @@ function renderMeetings(meetings) {
55
63
  '</div>';
56
64
  }).join('');
57
65
 
66
+ if (visible.length > MTG_PER_PAGE) {
67
+ el.innerHTML += '<div class="pr-pager">' +
68
+ '<span class="pr-page-info">Showing ' + (start + 1) + ' to ' + Math.min(start + MTG_PER_PAGE, visible.length) + ' of ' + visible.length + '</span>' +
69
+ '<div class="pr-pager-btns">' +
70
+ '<button class="pr-pager-btn ' + (_mtgPage === 0 ? 'disabled' : '') + '" onclick="_mtgPrev()">Prev</button>' +
71
+ '<button class="pr-pager-btn ' + (_mtgPage >= totalPages - 1 ? 'disabled' : '') + '" onclick="_mtgNext()">Next</button>' +
72
+ '</div></div>';
73
+ }
74
+
58
75
  if (archived.length > 0) {
59
76
  el.innerHTML += '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">' +
60
77
  (_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>';
61
78
  }
62
79
  }
63
80
 
81
+ function _mtgPrev() { if (_mtgPage > 0) { _mtgPage--; refresh(); } }
82
+ function _mtgNext() { _mtgPage++; refresh(); }
83
+
64
84
  function _toggleArchivedMeetings() {
65
85
  _showArchived = !_showArchived;
86
+ _mtgPage = 0;
66
87
  refresh();
67
88
  }
68
89
 
@@ -207,37 +228,37 @@ async function _submitCreateMeeting() {
207
228
  const checks = document.querySelectorAll('#mtg-participants input[type="checkbox"]:checked');
208
229
  const participants = [...checks].map(c => c.value);
209
230
  if (participants.length < 2) { alert('Select at least 2 participants'); return; }
210
-
231
+ try { closeModal(); } catch { /* expected */ }
232
+ showToast('cmd-toast', 'Meeting started with ' + participants.length + ' agents', true);
211
233
  try {
212
234
  const res = await fetch('/api/meetings', {
213
235
  method: 'POST', headers: { 'Content-Type': 'application/json' },
214
236
  body: JSON.stringify({ title, agenda, participants })
215
237
  });
216
238
  const data = await res.json();
217
- if (res.ok) {
218
- try { closeModal(); } catch { /* expected */ }
219
- wakeEngine();
220
- refresh();
221
- try { showToast('cmd-toast', 'Meeting started with ' + participants.length + ' agents', true); } catch { /* expected */ }
222
- } else { alert('Failed: ' + (data.error || 'unknown')); }
223
- } catch (e) { alert('Error: ' + e.message); }
239
+ if (res.ok) { wakeEngine(); refresh(); }
240
+ else { alert('Failed: ' + (data.error || 'unknown')); openCreateMeetingModal(); }
241
+ } catch (e) { alert('Error: ' + e.message); openCreateMeetingModal(); }
224
242
  }
225
243
 
226
244
  async function _submitMeetingNote(id) {
227
245
  const input = document.getElementById('meeting-note-input');
228
246
  if (!input?.value?.trim()) return;
247
+ const note = input.value.trim();
248
+ input.value = '';
229
249
  try {
230
- await fetch('/api/meetings/note', {
250
+ const res = await fetch('/api/meetings/note', {
231
251
  method: 'POST', headers: { 'Content-Type': 'application/json' },
232
- body: JSON.stringify({ id, note: input.value.trim() })
252
+ body: JSON.stringify({ id, note })
233
253
  });
234
- input.value = '';
235
- openMeetingDetail(id); // refresh the modal
236
- } catch (e) { alert('Error: ' + e.message); }
254
+ if (res.ok) openMeetingDetail(id);
255
+ else { input.value = note; alert('Failed to add note'); }
256
+ } catch (e) { input.value = note; alert('Error: ' + e.message); }
237
257
  }
238
258
 
239
259
  async function _advanceMeeting(id) {
240
260
  if (!confirm('Skip to next round? Agents that haven\'t finished will be skipped.')) return;
261
+ showToast('cmd-toast', 'Advancing to next round...', true);
241
262
  try {
242
263
  await fetch('/api/meetings/advance', {
243
264
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -250,38 +271,42 @@ async function _advanceMeeting(id) {
250
271
 
251
272
  async function _endMeeting(id) {
252
273
  if (!confirm('End this meeting? Current round will be stopped.')) return;
274
+ try { closeModal(); } catch { /* expected */ }
275
+ showToast('cmd-toast', 'Meeting ended', true);
253
276
  try {
254
277
  await fetch('/api/meetings/end', {
255
278
  method: 'POST', headers: { 'Content-Type': 'application/json' },
256
279
  body: JSON.stringify({ id })
257
280
  });
258
- try { closeModal(); } catch { /* expected */ }
259
281
  refresh();
260
- } catch (e) { alert('Error: ' + e.message); }
282
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
261
283
  }
262
284
 
263
285
  async function _archiveMeeting(id) {
286
+ markDeleted('mtg:' + id);
287
+ try { closeModal(); } catch { /* may not be open */ }
288
+ document.querySelectorAll('[onclick*="openMeetingDetail(\'' + id + '\')"]').forEach(function(el) { el.remove(); });
289
+ showToast('cmd-toast', 'Meeting archived', true);
264
290
  try {
265
291
  const res = await fetch('/api/meetings/archive', {
266
292
  method: 'POST', headers: { 'Content-Type': 'application/json' },
267
293
  body: JSON.stringify({ id })
268
294
  });
269
- if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Failed: ' + (d.error || 'unknown')); return; }
270
- try { closeModal(); } catch { /* may not be open */ }
271
- refresh();
272
- } catch (e) { alert('Error: ' + e.message); }
295
+ if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Failed: ' + (d.error || 'unknown')); refresh(); }
296
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
273
297
  }
274
298
 
275
299
  async function _unarchiveMeeting(id) {
300
+ try { closeModal(); } catch { /* may not be open */ }
301
+ document.querySelectorAll('[onclick*="openMeetingDetail(\'' + id + '\')"]').forEach(function(el) { el.remove(); });
302
+ showToast('cmd-toast', 'Meeting unarchived', true);
276
303
  try {
277
304
  const res = await fetch('/api/meetings/unarchive', {
278
305
  method: 'POST', headers: { 'Content-Type': 'application/json' },
279
306
  body: JSON.stringify({ id })
280
307
  });
281
- if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Failed: ' + (d.error || 'unknown')); return; }
282
- try { closeModal(); } catch { /* may not be open */ }
283
- refresh();
284
- } catch (e) { alert('Error: ' + e.message); }
308
+ if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Delete failed: ' + (d.error || 'unknown')); refresh(); }
309
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
285
310
  }
286
311
 
287
312
  function _viewPlanWithBack(file, meetingId) {
@@ -382,15 +407,16 @@ async function _createPlanFromMeeting(id, btn) {
382
407
 
383
408
  async function _deleteMeeting(id) {
384
409
  if (!confirm('Delete this meeting? This cannot be undone.')) return;
410
+ markDeleted('mtg:' + id);
411
+ try { closeModal(); } catch { /* may not be open */ }
412
+ document.querySelectorAll('[onclick*="openMeetingDetail(\'' + id + '\')"]').forEach(function(el) { el.remove(); });
385
413
  try {
386
414
  const res = await fetch('/api/meetings/delete', {
387
415
  method: 'POST', headers: { 'Content-Type': 'application/json' },
388
416
  body: JSON.stringify({ id })
389
417
  });
390
- if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Failed: ' + (d.error || 'unknown')); return; }
391
- try { closeModal(); } catch { /* may not be open */ }
392
- refresh();
393
- } catch (e) { alert('Error: ' + e.message); }
418
+ if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Failed: ' + (d.error || 'unknown')); refresh(); }
419
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
394
420
  }
395
421
 
396
422
  window.MinionsMeetings = { renderMeetings, openMeetingDetail, openCreateMeetingModal };
@@ -1,6 +1,7 @@
1
1
  // dashboard/js/render-pinned.js — Pinned context notes rendering and management
2
2
 
3
3
  function renderPinned(entries) {
4
+ entries = (entries || []).filter(function(e) { return !isDeleted('pin:' + e.title); });
4
5
  const el = document.getElementById('pinned-content');
5
6
  if (!el) return;
6
7
  if (!entries || entries.length === 0) {
@@ -37,19 +38,22 @@ async function submitPinnedNote() {
37
38
  const content = document.getElementById('pin-content').value;
38
39
  const level = document.getElementById('pin-level').value;
39
40
  if (!title || !content) { alert('Title and content required'); return; }
41
+ try { closeModal(); } catch { /* may not be open */ }
42
+ showToast('cmd-toast', 'Note pinned', true);
40
43
  try {
41
44
  const res = await fetch('/api/pinned', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content, level }) });
42
- if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Note pinned', true); }
43
- else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
44
- } catch (e) { alert('Error: ' + e.message); }
45
+ if (res.ok) { refresh(); } else { const d = await res.json().catch(() => ({})); alert('Pin failed: ' + (d.error || 'unknown')); openPinNoteModal(); }
46
+ } catch (e) { alert('Error: ' + e.message); openPinNoteModal(); }
45
47
  }
46
48
 
47
49
  async function removePinnedNote(title) {
48
50
  if (!confirm('Unpin "' + title + '"?')) return;
51
+ markDeleted('pin:' + title);
52
+ const btn = event?.target; if (btn) { const card = btn.closest('.pinned-card') || btn.parentElement?.parentElement; if (card) card.remove(); }
49
53
  try {
50
- await fetch('/api/pinned/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) });
51
- refresh();
52
- } catch (e) { alert('Error: ' + e.message); }
54
+ const res = await fetch('/api/pinned/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) });
55
+ if (!res.ok) { alert('Unpin failed'); refresh(); }
56
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
53
57
  }
54
58
 
55
59
  window.MinionsPinned = { renderPinned, openPinNoteModal, submitPinnedNote, removePinnedNote };
@@ -130,20 +130,16 @@ async function _submitLinkPr() {
130
130
  const context = document.getElementById('pr-link-context')?.value || '';
131
131
  const autoObserve = document.getElementById('pr-link-observe')?.checked || false;
132
132
 
133
+ try { closeModal(); } catch { /* expected */ }
134
+ showToast('cmd-toast', 'PR linked' + (autoObserve ? ' (auto-observe on)' : ''), true);
133
135
  try {
134
136
  const res = await fetch('/api/pull-requests/link', {
135
137
  method: 'POST', headers: { 'Content-Type': 'application/json' },
136
138
  body: JSON.stringify({ url, title, project, context, autoObserve })
137
139
  });
138
140
  const data = await res.json();
139
- if (res.ok) {
140
- try { closeModal(); } catch { /* expected */ }
141
- refresh();
142
- try { showToast('cmd-toast', 'PR ' + (data.id || '') + ' linked' + (autoObserve ? ' (auto-observe on)' : ''), true); } catch { /* expected */ }
143
- } else {
144
- alert('Failed: ' + (data.error || 'unknown'));
145
- }
146
- } catch (e) { alert('Error: ' + e.message); }
141
+ if (res.ok) { refresh(); } else { alert('Failed: ' + (data.error || 'unknown')); openAddPrModal(); }
142
+ } catch (e) { alert('Error: ' + e.message); openAddPrModal(); }
147
143
  }
148
144
 
149
145
  window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal };
@@ -181,6 +181,7 @@ let _schedPage = 0;
181
181
  const SCHED_PER_PAGE = 15;
182
182
 
183
183
  function renderSchedules(schedules) {
184
+ schedules = schedules.filter(function(s) { return !isDeleted('sched:' + s.id); });
184
185
  const el = document.getElementById('scheduled-content');
185
186
  const countEl = document.getElementById('scheduled-count');
186
187
  countEl.textContent = schedules.length;
@@ -431,6 +432,8 @@ async function submitSchedule(isEdit) {
431
432
  }
432
433
 
433
434
  async function toggleScheduleEnabled(id, enabled) {
435
+ // Optimistic toggle — swap badge text immediately
436
+ document.querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) { var badge = r.querySelector('.status-badge'); if (badge) badge.textContent = enabled ? 'ENABLED' : 'DISABLED'; } });
434
437
  try {
435
438
  const res = await fetch('/api/schedules/update', {
436
439
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -438,23 +441,23 @@ async function toggleScheduleEnabled(id, enabled) {
438
441
  });
439
442
  if (res.ok) { refresh(); } else {
440
443
  const d = await res.json().catch(() => ({}));
441
- _showScheduleError('Toggle failed: ' + (d.error || 'unknown'));
444
+ _showScheduleError('Toggle failed: ' + (d.error || 'unknown')); refresh();
442
445
  }
443
- } catch (e) { _showScheduleError('Toggle error: ' + e.message); }
446
+ } catch (e) { _showScheduleError('Toggle error: ' + e.message); refresh(); }
444
447
  }
445
448
 
446
449
  async function deleteSchedule(id) {
447
450
  if (!confirm('Delete scheduled task "' + id + '"?')) return;
451
+ markDeleted('sched:' + id);
452
+ document.querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
453
+ showToast('cmd-toast', 'Schedule deleted', true);
448
454
  try {
449
455
  const res = await fetch('/api/schedules/delete', {
450
456
  method: 'POST', headers: { 'Content-Type': 'application/json' },
451
457
  body: JSON.stringify({ id })
452
458
  });
453
- if (res.ok) { refresh(); showToast('cmd-toast', 'Schedule deleted', true); } else {
454
- const d = await res.json().catch(() => ({}));
455
- _showScheduleError('Delete failed: ' + (d.error || 'unknown'));
456
- }
457
- } catch (e) { _showScheduleError('Delete error: ' + e.message); }
459
+ if (!res.ok) { const d = await res.json().catch(() => ({})); _showScheduleError('Delete failed: ' + (d.error || 'unknown')); refresh(); }
460
+ } catch (e) { _showScheduleError('Delete error: ' + e.message); refresh(); }
458
461
  }
459
462
 
460
463
  // Expose _generateScheduleId globally for the inline oninput handler
@@ -47,6 +47,7 @@ function wiRow(item) {
47
47
  }
48
48
 
49
49
  function renderWorkItems(items) {
50
+ items = items.filter(function(w) { return !isDeleted('wi:' + w.id); });
50
51
  // Sort: active/dispatched first, then by most recent activity
51
52
  const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
52
53
  items.sort((a, b) => {
@@ -146,43 +147,41 @@ async function submitWorkItemEdit(id, source) {
146
147
  const acRaw = document.getElementById('wi-edit-ac')?.value || '';
147
148
  const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
148
149
  if (!title) { alert('Title is required'); return; }
150
+ try { closeModal(); } catch { /* may not be open */ }
151
+ showToast('cmd-toast', 'Work item updated', true);
149
152
  try {
150
153
  const res = await fetch('/api/work-items/update', {
151
154
  method: 'POST', headers: { 'Content-Type': 'application/json' },
152
155
  body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent, references, acceptanceCriteria })
153
156
  });
154
- if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Work item updated', true); } else {
155
- const d = await res.json();
156
- alert('Update failed: ' + (d.error || 'unknown'));
157
- }
158
- } catch (e) { alert('Update error: ' + e.message); }
157
+ if (res.ok) { refresh(); } else { const d = await res.json().catch(() => ({})); alert('Update failed: ' + (d.error || 'unknown')); editWorkItem(id, source); }
158
+ } catch (e) { alert('Update error: ' + e.message); editWorkItem(id, source); }
159
159
  }
160
160
 
161
161
  async function deleteWorkItem(id, source) {
162
162
  if (!confirm('Delete work item ' + id + '? This will kill any running agent and remove all dispatch history.')) return;
163
+ markDeleted('wi:' + id);
164
+ document.querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
163
165
  try {
164
166
  const res = await fetch('/api/work-items/delete', {
165
167
  method: 'POST', headers: { 'Content-Type': 'application/json' },
166
168
  body: JSON.stringify({ id, source: source || undefined })
167
169
  });
168
- if (res.ok) { refresh(); } else {
169
- const d = await res.json();
170
- alert('Delete failed: ' + (d.error || 'unknown'));
171
- }
172
- } catch (e) { alert('Delete error: ' + e.message); }
170
+ if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Delete failed: ' + (d.error || 'unknown')); refresh(); }
171
+ } catch (e) { alert('Delete error: ' + e.message); refresh(); }
173
172
  }
174
173
 
175
174
  async function archiveWorkItem(id, source) {
175
+ markDeleted('wi:' + id);
176
+ document.querySelectorAll('tr').forEach(function(r) { if (r.textContent.includes(id)) r.remove(); });
177
+ showToast('cmd-toast', 'Archived ' + id, true);
176
178
  try {
177
179
  const res = await fetch('/api/work-items/archive', {
178
180
  method: 'POST', headers: { 'Content-Type': 'application/json' },
179
181
  body: JSON.stringify({ id, source: source || undefined })
180
182
  });
181
- if (res.ok) { refresh(); } else {
182
- const d = await res.json();
183
- alert('Archive failed: ' + (d.error || 'unknown'));
184
- }
185
- } catch (e) { alert('Archive error: ' + e.message); }
183
+ if (!res.ok) { const d = await res.json().catch(() => ({})); alert('Archive failed: ' + (d.error || 'unknown')); refresh(); return; }
184
+ } catch (e) { alert('Archive error: ' + e.message); refresh(); }
186
185
  }
187
186
 
188
187
  let wiArchiveVisible = false;
@@ -269,13 +268,14 @@ async function submitFeedback(id, source) {
269
268
  const rating = _feedbackRating;
270
269
  if (!rating) { alert('Please select a rating first'); return; }
271
270
  const comment = document.getElementById('feedback-comment')?.value || '';
271
+ try { closeModal(); } catch { /* may not be open */ }
272
+ showToast('cmd-toast', 'Feedback saved — agents will learn from it', true);
272
273
  try {
273
274
  const res = await fetch('/api/work-items/feedback', {
274
275
  method: 'POST', headers: { 'Content-Type': 'application/json' },
275
276
  body: JSON.stringify({ id, source, rating, comment })
276
277
  });
277
- if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Feedback saved agents will learn from it', true); }
278
- else { const d = await res.json(); alert('Error: ' + (d.error || 'unknown')); }
278
+ if (res.ok) { refresh(); } else { const d = await res.json().catch(() => ({})); alert('Feedback failed: ' + (d.error || 'unknown')); }
279
279
  } catch (e) { alert('Error: ' + e.message); }
280
280
  }
281
281
 
@@ -341,20 +341,22 @@ async function _submitCreateWorkItem() {
341
341
  if (acceptanceCriteria.length) body.acceptanceCriteria = acceptanceCriteria;
342
342
  if (references.length && references[0].url) body.references = references;
343
343
 
344
+ try { closeModal(); } catch { /* expected */ }
345
+ showToast('cmd-toast', 'Creating work item...', true);
344
346
  const res = await fetch('/api/work-items', {
345
347
  method: 'POST', headers: { 'Content-Type': 'application/json' },
346
348
  body: JSON.stringify(body)
347
349
  });
348
350
  const data = await res.json();
349
351
  if (res.ok) {
350
- try { closeModal(); } catch { /* expected */ }
351
352
  wakeEngine();
352
353
  refresh();
353
- try { showToast('cmd-toast', 'Work item ' + (data.id || '') + ' created', true); } catch { /* expected */ }
354
+ showToast('cmd-toast', 'Work item ' + (data.id || '') + ' created', true);
354
355
  } else {
355
356
  alert('Failed: ' + (data.error || 'unknown'));
357
+ openCreateWorkItemModal();
356
358
  }
357
- } catch (e) { alert('Error: ' + e.message); }
359
+ } catch (e) { alert('Error: ' + e.message); openCreateWorkItemModal(); }
358
360
  }
359
361
 
360
362
  function openWorkItemDetail(id) {
@@ -32,6 +32,11 @@ async function openSettings() {
32
32
  settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
33
33
  settingsField('Worktree Create Retries', 'set-worktreeCreateRetries', e.worktreeCreateRetries || 1, '', 'Retry count for transient worktree add failures (0-3)') +
34
34
  '</div>' +
35
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px">' +
36
+ settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review') +
37
+ settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
38
+ settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
39
+ '</div>' +
35
40
 
36
41
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
37
42
  '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
@@ -72,6 +77,14 @@ async function openSettings() {
72
77
  document.getElementById('modal').classList.add('open');
73
78
  }
74
79
 
80
+ function settingsToggle(label, id, checked, hint) {
81
+ return '<div style="display:flex;align-items:center;gap:8px;padding:4px 0">' +
82
+ '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
83
+ '<label for="' + id + '" style="font-size:12px;color:var(--text);cursor:pointer">' + escHtml(label) + '</label>' +
84
+ (hint ? '<span style="font-size:9px;color:var(--muted)">' + escHtml(hint) + '</span>' : '') +
85
+ '</div>';
86
+ }
87
+
75
88
  function settingsField(label, id, value, unit, hint) {
76
89
  return '<div>' +
77
90
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">' + escHtml(label) + (unit ? ' <span style="opacity:0.6">(' + escHtml(unit) + ')</span>' : '') + '</label>' +
@@ -95,6 +108,9 @@ async function saveSettings() {
95
108
  heartbeatTimeout: document.getElementById('set-heartbeatTimeout').value,
96
109
  worktreeCreateTimeout: document.getElementById('set-worktreeCreateTimeout').value,
97
110
  worktreeCreateRetries: document.getElementById('set-worktreeCreateRetries').value,
111
+ autoApprovePlans: document.getElementById('set-autoApprovePlans').checked,
112
+ autoDecompose: document.getElementById('set-autoDecompose').checked,
113
+ allowTempAgents: document.getElementById('set-allowTempAgents').checked,
98
114
  };
99
115
 
100
116
  const claudePayload = {
@@ -3,6 +3,11 @@
3
3
  // Signal the engine to tick immediately (pick up new work without waiting 60s)
4
4
  function wakeEngine() { fetch('/api/engine/wakeup', { method: 'POST' }).catch(() => {}); }
5
5
 
6
+ // Optimistic delete suppression — prevent auto-refresh from re-showing deleted items
7
+ const _deletedIds = new Map(); // key → expiry timestamp
8
+ function markDeleted(key) { _deletedIds.set(key, Date.now() + 10000); } // suppress for 10s
9
+ function isDeleted(key) { const exp = _deletedIds.get(key); if (!exp) return false; if (Date.now() > exp) { _deletedIds.delete(key); return false; } return true; }
10
+
6
11
  function escHtml(s) {
7
12
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
8
13
  }
package/dashboard.js CHANGED
@@ -3065,6 +3065,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3065
3065
  if (e.heartbeatTimeout !== undefined) config.engine.heartbeatTimeout = Math.max(60000, Number(e.heartbeatTimeout) || D.heartbeatTimeout);
3066
3066
  if (e.worktreeCreateTimeout !== undefined) config.engine.worktreeCreateTimeout = Math.max(60000, Number(e.worktreeCreateTimeout) || D.worktreeCreateTimeout);
3067
3067
  if (e.worktreeCreateRetries !== undefined) config.engine.worktreeCreateRetries = Math.max(0, Math.min(3, Number(e.worktreeCreateRetries) || D.worktreeCreateRetries));
3068
+ if (e.autoApprovePlans !== undefined) config.engine.autoApprovePlans = !!e.autoApprovePlans;
3069
+ if (e.autoDecompose !== undefined) config.engine.autoDecompose = !!e.autoDecompose;
3070
+ if (e.allowTempAgents !== undefined) config.engine.allowTempAgents = !!e.allowTempAgents;
3068
3071
  }
3069
3072
 
3070
3073
  if (body.claude) {
package/engine/shared.js CHANGED
@@ -336,6 +336,7 @@ const ENGINE_DEFAULTS = {
336
336
  shutdownTimeout: 300000, // 5min — max wait for active agents during graceful shutdown
337
337
  allowTempAgents: false, // opt-in: spawn ephemeral agents when all permanent agents are busy
338
338
  autoDecompose: true, // auto-decompose implement:large items into sub-tasks
339
+ autoApprovePlans: false, // auto-approve PRDs without waiting for human approval
339
340
  meetingRoundTimeout: 600000, // 10min per meeting round before auto-advance
340
341
  };
341
342
 
package/engine.js CHANGED
@@ -1031,8 +1031,19 @@ function materializePlansAsWorkItems(config) {
1031
1031
  // Human approval gate: plans start as 'awaiting-approval' and must be approved before work begins
1032
1032
  // Plans without a status (legacy) or with status 'approved' are allowed through
1033
1033
  const planStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
1034
- if (planStatus === 'awaiting-approval' || planStatus === 'paused' || planStatus === 'rejected' || planStatus === 'revision-requested') {
1035
- continue; // Skip — waiting for human approval, paused, or revision
1034
+ if (planStatus === 'awaiting-approval') {
1035
+ if (config.engine?.autoApprovePlans) {
1036
+ plan.status = 'approved';
1037
+ plan.approvedAt = new Date().toISOString();
1038
+ plan.approvedBy = 'auto-mode';
1039
+ safeWrite(prdPath, plan);
1040
+ log('info', `Auto-approved plan: ${file}`);
1041
+ } else {
1042
+ continue; // Skip — waiting for human approval
1043
+ }
1044
+ }
1045
+ if (planStatus === 'paused' || planStatus === 'rejected' || planStatus === 'revision-requested') {
1046
+ continue; // Skip — paused or revision requested
1036
1047
  }
1037
1048
  // Stale PRDs: source plan was revised — don't materialize NEW items until user regenerates
1038
1049
  if (plan.planStale) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
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"