@yemi33/minions 0.1.179 → 0.1.181

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.181 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine/pipeline.js
7
+
8
+ ## 0.1.180 (2026-04-02)
9
+
10
+ ### Engine
11
+ - engine.js
12
+ - engine/ado.js
13
+ - engine/cleanup.js
14
+ - engine/lifecycle.js
15
+ - engine/meeting.js
16
+ - engine/routing.js
17
+ - engine/shared.js
18
+ - engine/spawn-agent.js
19
+
20
+ ### Dashboard
21
+ - dashboard/js/render-dispatch.js
22
+ - dashboard/js/render-inbox.js
23
+ - dashboard/js/render-kb.js
24
+ - dashboard/js/render-meetings.js
25
+ - dashboard/js/render-pinned.js
26
+ - dashboard/js/render-pipelines.js
27
+ - dashboard/js/render-plans.js
28
+ - dashboard/js/render-prd.js
29
+ - dashboard/js/render-prs.js
30
+ - dashboard/js/render-schedules.js
31
+ - dashboard/js/render-work-items.js
32
+ - dashboard/js/state.js
33
+
34
+ ### Other
35
+ - test/unit.test.js
36
+
3
37
  ## 0.1.179 (2026-04-02)
4
38
 
5
39
  ### Dashboard
@@ -1,5 +1,15 @@
1
1
  // dashboard/js/render-dispatch.js — Engine status, dispatch, and log rendering extracted from dashboard.html
2
2
 
3
+ const COMPLETED_PER_PAGE = 20;
4
+ const LOG_PER_PAGE = 50;
5
+ let _completedPage = 0;
6
+ let _logPage = 0;
7
+
8
+ function _completedPrev() { if (_completedPage > 0) { _completedPage--; refresh(); } }
9
+ function _completedNext() { _completedPage++; refresh(); }
10
+ function _logPrev() { if (_logPage > 0) { _logPage--; refresh(); } }
11
+ function _logNext() { _logPage++; refresh(); }
12
+
3
13
  function renderEngineStatus(engine) {
4
14
  const badge = document.getElementById('engine-badge');
5
15
  let state = engine?.state || 'stopped';
@@ -106,8 +116,14 @@ function renderDispatch(dispatch) {
106
116
  completedCount.textContent = completed.length;
107
117
 
108
118
  if (completed.length > 0) {
119
+ const totalCompPages = Math.ceil(completed.length / COMPLETED_PER_PAGE);
120
+ if (_completedPage >= totalCompPages) _completedPage = totalCompPages - 1;
121
+ if (_completedPage < 0) _completedPage = 0;
122
+ const compStart = _completedPage * COMPLETED_PER_PAGE;
123
+ const pageCompleted = completed.slice(compStart, compStart + COMPLETED_PER_PAGE);
124
+
109
125
  completedEl.innerHTML = '<table class="pr-table"><thead><tr><th>ID</th><th>Type</th><th>Agent</th><th>Task</th><th>Result</th><th>Completed</th></tr></thead><tbody>' +
110
- completed.map(d => {
126
+ pageCompleted.map(d => {
111
127
  const isError = d.result === 'error';
112
128
  const agentId = (d.agent || '').toLowerCase();
113
129
  const errorBtn = isError
@@ -122,6 +138,14 @@ function renderDispatch(dispatch) {
122
138
  '<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
123
139
  '</tr>';
124
140
  }).join('') + '</tbody></table>';
141
+ if (completed.length > COMPLETED_PER_PAGE) {
142
+ completedEl.innerHTML += '<div class="pr-pager">' +
143
+ '<span class="pr-page-info">Showing ' + (compStart + 1) + ' to ' + Math.min(compStart + COMPLETED_PER_PAGE, completed.length) + ' of ' + completed.length + '</span>' +
144
+ '<div class="pr-pager-btns">' +
145
+ '<button class="pr-pager-btn ' + (_completedPage === 0 ? 'disabled' : '') + '" onclick="_completedPrev()">Prev</button>' +
146
+ '<button class="pr-pager-btn ' + (_completedPage >= totalCompPages - 1 ? 'disabled' : '') + '" onclick="_completedNext()">Next</button>' +
147
+ '</div></div>';
148
+ }
125
149
  } else {
126
150
  completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
127
151
  }
@@ -133,13 +157,28 @@ function renderEngineLog(log) {
133
157
  el.innerHTML = '<div class="empty">No log entries yet.</div>';
134
158
  return;
135
159
  }
136
- el.innerHTML = log.slice().reverse().map(e =>
160
+ const reversed = log.slice().reverse();
161
+ const totalLogPages = Math.ceil(reversed.length / LOG_PER_PAGE);
162
+ if (_logPage >= totalLogPages) _logPage = totalLogPages - 1;
163
+ if (_logPage < 0) _logPage = 0;
164
+ const logStart = _logPage * LOG_PER_PAGE;
165
+ const pageLog = reversed.slice(logStart, logStart + LOG_PER_PAGE);
166
+
167
+ el.innerHTML = pageLog.map(e =>
137
168
  '<div class="log-entry">' +
138
169
  '<span class="log-ts">' + shortTime(e.timestamp) + '</span> ' +
139
170
  '<span class="log-level-' + (e.level || 'info') + '">[' + (e.level || 'info') + ']</span> ' +
140
171
  escHtml(e.message || '') +
141
172
  '</div>'
142
173
  ).join('');
174
+ if (reversed.length > LOG_PER_PAGE) {
175
+ el.innerHTML += '<div class="pr-pager">' +
176
+ '<span class="pr-page-info">Showing ' + (logStart + 1) + ' to ' + Math.min(logStart + LOG_PER_PAGE, reversed.length) + ' of ' + reversed.length + '</span>' +
177
+ '<div class="pr-pager-btns">' +
178
+ '<button class="pr-pager-btn ' + (_logPage === 0 ? 'disabled' : '') + '" onclick="_logPrev()">Prev</button>' +
179
+ '<button class="pr-pager-btn ' + (_logPage >= totalLogPages - 1 ? 'disabled' : '') + '" onclick="_logNext()">Next</button>' +
180
+ '</div></div>';
181
+ }
143
182
  }
144
183
 
145
184
  function shortTime(t) {
@@ -1,5 +1,11 @@
1
1
  // render-inbox.js — Inbox and notes rendering functions extracted from dashboard.html
2
2
 
3
+ const INBOX_PER_PAGE = 15;
4
+ let _inboxPage = 0;
5
+
6
+ function _inboxPrev() { if (_inboxPage > 0) { _inboxPage--; renderInbox(inboxData); } }
7
+ function _inboxNext() { _inboxPage++; renderInbox(inboxData); }
8
+
3
9
  function renderInbox(inbox) {
4
10
  inbox = inbox.filter(function(item) { return !isDeleted('inbox:' + item.name); });
5
11
  inboxData = inbox;
@@ -7,19 +13,35 @@ function renderInbox(inbox) {
7
13
  const count = document.getElementById('inbox-count');
8
14
  count.textContent = inbox.length;
9
15
  if (!inbox.length) { list.innerHTML = '<p class="empty">No messages yet.</p>'; return; }
10
- list.innerHTML = inbox.map((item, i) => `
11
- <div class="inbox-item" data-file="notes/inbox/${escHtml(item.name)}">
12
- <div class="inbox-name" onclick="openModal(${i})" style="cursor:pointer">
16
+
17
+ const totalInboxPages = Math.ceil(inbox.length / INBOX_PER_PAGE);
18
+ if (_inboxPage >= totalInboxPages) _inboxPage = totalInboxPages - 1;
19
+ if (_inboxPage < 0) _inboxPage = 0;
20
+ const inboxStart = _inboxPage * INBOX_PER_PAGE;
21
+ const pageInbox = inbox.slice(inboxStart, inboxStart + INBOX_PER_PAGE);
22
+
23
+ list.innerHTML = pageInbox.map((item, i) => {
24
+ const idx = inboxStart + i;
25
+ return `<div class="inbox-item" data-file="notes/inbox/${escHtml(item.name)}">
26
+ <div class="inbox-name" onclick="openModal(${idx})" style="cursor:pointer">
13
27
  <span>${escHtml(item.name)}</span><span>${item.age}</span>
14
28
  </div>
15
- <div class="inbox-preview" onclick="openModal(${i})" style="cursor:pointer">${escHtml(item.content.slice(0,200))}</div>
29
+ <div class="inbox-preview" onclick="openModal(${idx})" style="cursor:pointer">${escHtml(item.content.slice(0,200))}</div>
16
30
  <div style="display:flex;gap:6px;margin-top:6px;align-items:center">
17
31
  <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();promoteToKB('${escHtml(item.name)}')">Add to Knowledge Base</button>
18
32
  <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();openInboxInExplorer('${escHtml(item.name)}')">Open in Explorer</button>
19
33
  <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" onclick="event.stopPropagation();deleteInboxItem('${escHtml(item.name)}')">Delete</button>
20
34
  </div>
21
- </div>
22
- `).join('');
35
+ </div>`;
36
+ }).join('');
37
+ if (inbox.length > INBOX_PER_PAGE) {
38
+ list.innerHTML += '<div class="pr-pager">' +
39
+ '<span class="pr-page-info">Showing ' + (inboxStart + 1) + ' to ' + Math.min(inboxStart + INBOX_PER_PAGE, inbox.length) + ' of ' + inbox.length + '</span>' +
40
+ '<div class="pr-pager-btns">' +
41
+ '<button class="pr-pager-btn ' + (_inboxPage === 0 ? 'disabled' : '') + '" onclick="_inboxPrev()">Prev</button>' +
42
+ '<button class="pr-pager-btn ' + (_inboxPage >= totalInboxPages - 1 ? 'disabled' : '') + '" onclick="_inboxNext()">Next</button>' +
43
+ '</div></div>';
44
+ }
23
45
  restoreNotifBadges();
24
46
  }
25
47
 
@@ -13,6 +13,11 @@ const KB_CAT_ICONS = {
13
13
  incidents: '\u{1F6A8}', 'api-notes': '\u{1F517}',
14
14
  };
15
15
  let _kbActiveTab = 'all';
16
+ const KB_PER_PAGE = 30;
17
+ let _kbPage = 0;
18
+
19
+ function _kbPrev() { if (_kbPage > 0) { _kbPage--; renderKnowledgeBase(); } }
20
+ function _kbNext() { _kbPage++; renderKnowledgeBase(); }
16
21
 
17
22
  async function refreshKnowledgeBase() {
18
23
  try {
@@ -67,7 +72,13 @@ function renderKnowledgeBase() {
67
72
  return;
68
73
  }
69
74
 
70
- listEl.innerHTML = items.slice(0, 50).map(item => {
75
+ const totalKbPages = Math.ceil(items.length / KB_PER_PAGE);
76
+ if (_kbPage >= totalKbPages) _kbPage = totalKbPages - 1;
77
+ if (_kbPage < 0) _kbPage = 0;
78
+ const kbStart = _kbPage * KB_PER_PAGE;
79
+ const pageItems = items.slice(kbStart, kbStart + KB_PER_PAGE);
80
+
81
+ listEl.innerHTML = pageItems.map(item => {
71
82
  const icon = KB_CAT_ICONS[item.category] || '\u{1F4C4}';
72
83
  const label = KB_CAT_LABELS[item.category] || item.category;
73
84
  return '<div class="kb-item" data-file="knowledge/' + escHtml(item.category) + '/' + escHtml(item.file) + '" onclick="kbOpenItem(\'' + escHtml(item.category) + '\', \'' + escHtml(item.file) + '\')">' +
@@ -83,11 +94,20 @@ function renderKnowledgeBase() {
83
94
  '</div>' +
84
95
  '</div>';
85
96
  }).join('');
97
+ if (items.length > KB_PER_PAGE) {
98
+ listEl.innerHTML += '<div class="pr-pager">' +
99
+ '<span class="pr-page-info">Showing ' + (kbStart + 1) + ' to ' + Math.min(kbStart + KB_PER_PAGE, items.length) + ' of ' + items.length + '</span>' +
100
+ '<div class="pr-pager-btns">' +
101
+ '<button class="pr-pager-btn ' + (_kbPage === 0 ? 'disabled' : '') + '" onclick="_kbPrev()">Prev</button>' +
102
+ '<button class="pr-pager-btn ' + (_kbPage >= totalKbPages - 1 ? 'disabled' : '') + '" onclick="_kbNext()">Next</button>' +
103
+ '</div></div>';
104
+ }
86
105
  restoreNotifBadges();
87
106
  }
88
107
 
89
108
  function kbSetTab(tab) {
90
109
  _kbActiveTab = tab;
110
+ _kbPage = 0;
91
111
  renderKnowledgeBase();
92
112
  }
93
113
 
@@ -143,10 +163,11 @@ function openCreateKbModal() {
143
163
  }
144
164
 
145
165
  async function submitKbEntry() {
166
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
146
167
  const category = document.getElementById('kb-new-category').value;
147
168
  const title = document.getElementById('kb-new-title').value;
148
169
  const content = document.getElementById('kb-new-content').value;
149
- if (!title || !content) { alert('Title and content are required'); return; }
170
+ if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Create'; } alert('Title and content are required'); return; }
150
171
  try {
151
172
  const res = await fetch('/api/knowledge', {
152
173
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -250,12 +250,13 @@ function openCreateMeetingModal() {
250
250
  }
251
251
 
252
252
  async function _submitCreateMeeting() {
253
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
253
254
  const title = document.getElementById('mtg-title')?.value?.trim();
254
255
  const agenda = document.getElementById('mtg-agenda')?.value?.trim();
255
- if (!title || !agenda) { alert('Title and agenda required'); return; }
256
+ if (!title || !agenda) { if (btn) { btn.disabled = false; btn.textContent = 'Start Meeting'; } alert('Title and agenda required'); return; }
256
257
  const checks = document.querySelectorAll('#mtg-participants input[type="checkbox"]:checked');
257
258
  const participants = [...checks].map(c => c.value);
258
- if (participants.length < 2) { alert('Select at least 2 participants'); return; }
259
+ if (participants.length < 2) { if (btn) { btn.disabled = false; btn.textContent = 'Start Meeting'; } alert('Select at least 2 participants'); return; }
259
260
  try { closeModal(); } catch { /* expected */ }
260
261
  showToast('cmd-toast', 'Meeting started with ' + participants.length + ' agents', true);
261
262
  try {
@@ -34,10 +34,11 @@ function openPinNoteModal() {
34
34
  }
35
35
 
36
36
  async function submitPinnedNote() {
37
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Pinning...'; }
37
38
  const title = document.getElementById('pin-title').value;
38
39
  const content = document.getElementById('pin-content').value;
39
40
  const level = document.getElementById('pin-level').value;
40
- if (!title || !content) { alert('Title and content required'); return; }
41
+ if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Pin Note'; } alert('Title and content required'); return; }
41
42
  try { closeModal(); } catch { /* may not be open */ }
42
43
  showToast('cmd-toast', 'Note pinned', true);
43
44
  try {
@@ -1,6 +1,11 @@
1
1
  // render-pipelines.js — Pipeline list, run detail, and create modal
2
2
 
3
3
  let _pipelinesData = [];
4
+ const PIPELINES_PER_PAGE = 10;
5
+ let _pipelinesPage = 0;
6
+
7
+ function _pipelinesPrev() { if (_pipelinesPage > 0) { _pipelinesPage--; refresh(); } }
8
+ function _pipelinesNext() { _pipelinesPage++; refresh(); }
4
9
 
5
10
  /**
6
11
  * Render clickable artifact links for a pipeline stage.
@@ -77,7 +82,24 @@ function renderPipelines(pipelines) {
77
82
  }
78
83
  countEl.textContent = pipelines.length;
79
84
 
80
- el.innerHTML = pipelines.map(function(p) {
85
+ const totalPipelinePages = Math.ceil(pipelines.length / PIPELINES_PER_PAGE);
86
+ if (_pipelinesPage >= totalPipelinePages) _pipelinesPage = totalPipelinePages - 1;
87
+ if (_pipelinesPage < 0) _pipelinesPage = 0;
88
+ const pipStart = _pipelinesPage * PIPELINES_PER_PAGE;
89
+ const pagePipelines = pipelines.slice(pipStart, pipStart + PIPELINES_PER_PAGE);
90
+
91
+ var pipelinePagerHtml = '';
92
+ if (pipelines.length > PIPELINES_PER_PAGE) {
93
+ pipelinePagerHtml = '<div class="pr-pager">' +
94
+ '<span class="pr-page-info">' + (pipStart + 1) + '-' + Math.min(pipStart + PIPELINES_PER_PAGE, pipelines.length) + ' of ' + pipelines.length + '</span>' +
95
+ '<div class="pr-pager-btns">' +
96
+ '<button class="pr-pager-btn ' + (_pipelinesPage === 0 ? 'disabled' : '') + '" onclick="_pipelinesPrev()">Prev</button>' +
97
+ '<button class="pr-pager-btn ' + (_pipelinesPage >= totalPipelinePages - 1 ? 'disabled' : '') + '" onclick="_pipelinesNext()">Next</button>' +
98
+ '</div>' +
99
+ '</div>';
100
+ }
101
+
102
+ el.innerHTML = pagePipelines.map(function(p) {
81
103
  const activeRun = (p.runs || []).find(function(r) { return r.status === 'running'; });
82
104
  const lastRun = (p.runs || []).slice(-1)[0];
83
105
  const statusColor = activeRun ? 'var(--blue)' : lastRun?.status === 'completed' ? 'var(--green)' : lastRun?.status === 'failed' ? 'var(--red)' : 'var(--muted)';
@@ -143,7 +165,7 @@ function renderPipelines(pipelines) {
143
165
  '<div style="margin-top:6px;display:flex;gap:4px;align-items:center;flex-wrap:wrap">' + stageFlow + '</div>' +
144
166
  progressHtml +
145
167
  '</div>';
146
- }).join('');
168
+ }).join('') + pipelinePagerHtml;
147
169
  }
148
170
 
149
171
  function openPipelineDetail(id) {
@@ -249,8 +271,8 @@ async function _togglePipelineEnabled(id, enabled, btn) {
249
271
  try {
250
272
  var res = await fetch('/api/pipelines/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id, enabled: enabled }) });
251
273
  if (res.ok) { showToast('cmd-toast', enabled ? 'Pipeline enabled' : 'Pipeline disabled', true); refresh(); }
252
- else { alert('Failed'); }
253
- } catch (e) { alert('Error: ' + e.message); }
274
+ else { alert('Failed'); refresh(); }
275
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
254
276
  if (btn) { btn.textContent = enabled ? 'Disable' : 'Enable'; btn.style.pointerEvents = ''; }
255
277
  }
256
278
 
@@ -349,12 +371,13 @@ function _updatePlCronPreview() {
349
371
  }
350
372
 
351
373
  async function _submitCreatePipeline() {
374
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
352
375
  var id = document.getElementById('pl-id')?.value?.trim();
353
376
  var title = document.getElementById('pl-title')?.value?.trim();
354
377
  var useCron = document.getElementById('pl-use-cron')?.checked;
355
378
  var cron = useCron ? (window._plComputedCron || '') : '';
356
379
  var stagesRaw = document.getElementById('pl-stages')?.value?.trim();
357
- if (!id || !title) { alert('ID and title required'); return; }
380
+ if (!id || !title) { if (btn) { btn.disabled = false; btn.textContent = 'Create Pipeline'; } alert('ID and title required'); return; }
358
381
  var stages;
359
382
  try { stages = JSON.parse(stagesRaw); } catch (e) { alert('Invalid JSON in stages: ' + e.message); return; }
360
383
  if (!Array.isArray(stages) || stages.length === 0) { alert('Stages must be a non-empty array'); return; }
@@ -1,5 +1,11 @@
1
1
  // render-plans.js — Plan rendering functions extracted from dashboard.html
2
2
 
3
+ const PLANS_PER_PAGE = 10;
4
+ let _plansPage = 0;
5
+
6
+ function _plansPrev() { if (_plansPage > 0) { _plansPage--; refresh(); } }
7
+ function _plansNext() { _plansPage++; refresh(); }
8
+
3
9
  function openCreatePlanModal() {
4
10
  const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p =>
5
11
  '<option value="' + escHtml(p) + '">' + escHtml(p) + '</option>'
@@ -23,10 +29,11 @@ function openCreatePlanModal() {
23
29
  }
24
30
 
25
31
  async function _submitCreatePlan() {
32
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
26
33
  const title = document.getElementById('plan-new-title')?.value?.trim();
27
34
  const content = document.getElementById('plan-new-content')?.value?.trim();
28
- if (!title) { alert('Title is required'); return; }
29
- if (!content) { alert('Plan content is required'); return; }
35
+ if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Title is required'); return; }
36
+ if (!content) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Plan content is required'); return; }
30
37
  const project = document.getElementById('plan-new-project')?.value || '';
31
38
 
32
39
  try {
@@ -90,6 +97,7 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
90
97
  }
91
98
 
92
99
  function renderPlans(plans) {
100
+ plans = plans.filter(function(p) { return !isDeleted('plan:' + p.file); });
93
101
  const el = document.getElementById('plans-list');
94
102
  const countEl = document.getElementById('plans-count');
95
103
  countEl.textContent = plans.length;
@@ -256,7 +264,23 @@ function renderPlans(plans) {
256
264
  '</div>';
257
265
  }
258
266
 
259
- let html = activePlans.map(renderPlanCard).join('');
267
+ const totalPlanPages = Math.ceil(activePlans.length / PLANS_PER_PAGE);
268
+ if (_plansPage >= totalPlanPages) _plansPage = totalPlanPages - 1;
269
+ if (_plansPage < 0) _plansPage = 0;
270
+ const plansStart = _plansPage * PLANS_PER_PAGE;
271
+ const pagePlans = activePlans.slice(plansStart, plansStart + PLANS_PER_PAGE);
272
+
273
+ let html = pagePlans.map(renderPlanCard).join('');
274
+
275
+ if (activePlans.length > PLANS_PER_PAGE) {
276
+ html += '<div class="pr-pager">' +
277
+ '<span class="pr-page-info">' + (plansStart + 1) + '-' + Math.min(plansStart + PLANS_PER_PAGE, activePlans.length) + ' of ' + activePlans.length + '</span>' +
278
+ '<div class="pr-pager-btns">' +
279
+ '<button class="pr-pager-btn ' + (_plansPage === 0 ? 'disabled' : '') + '" onclick="_plansPrev()">Prev</button>' +
280
+ '<button class="pr-pager-btn ' + (_plansPage >= totalPlanPages - 1 ? 'disabled' : '') + '" onclick="_plansNext()">Next</button>' +
281
+ '</div>' +
282
+ '</div>';
283
+ }
260
284
 
261
285
  if (archivedPlans.length > 0) {
262
286
  window._archivedPlans = archivedPlans;
@@ -493,21 +517,22 @@ async function planApprove(file, btn) {
493
517
  async function planDelete(file) {
494
518
  _stopPlanPoll();
495
519
  if (!confirm('Delete plan "' + file + '"? This cannot be undone.')) return;
520
+ markDeleted('plan:' + file);
521
+ closeModal();
522
+ showToast('cmd-toast', 'Plan deleted', true);
496
523
  try {
497
524
  const res = await fetch('/api/plans/delete', {
498
525
  method: 'POST', headers: { 'Content-Type': 'application/json' },
499
526
  body: JSON.stringify({ file })
500
527
  });
501
528
  if (res.ok) {
502
- closeModal();
503
- showToast('cmd-toast', 'Plan deleted', true);
504
- refreshPlans();
505
529
  refresh();
506
530
  } else {
507
- const d = await res.json();
508
- alert('Failed: ' + (d.error || 'unknown'));
531
+ const d = await res.json().catch(() => ({}));
532
+ alert('Delete failed: ' + (d.error || 'unknown'));
533
+ refresh(); // revert optimistic
509
534
  }
510
- } catch (e) { alert('Error: ' + e.message); }
535
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
511
536
  }
512
537
 
513
538
  async function planArchive(file, btn) {
@@ -528,7 +553,6 @@ async function planArchive(file, btn) {
528
553
  if (d.archivedSource) msg += ' PRD + source plan (' + d.archivedSource + ')';
529
554
  if (d.cancelledItems) msg += ', cancelled ' + d.cancelledItems + ' pending item(s)';
530
555
  showToast('cmd-toast', msg, true);
531
- refreshPlans();
532
556
  refresh();
533
557
  } else {
534
558
  resetBtn();
@@ -598,6 +598,7 @@ async function prdItemEdit(source, itemId) {
598
598
  }
599
599
 
600
600
  async function prdItemSave(source, itemId) {
601
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
601
602
  try {
602
603
  const res = await fetch('/api/prd-items/update', {
603
604
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -610,20 +611,22 @@ async function prdItemSave(source, itemId) {
610
611
  })
611
612
  });
612
613
  if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Item updated', true); }
613
- else { const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
614
- } catch (e) { alert('Error: ' + e.message); }
614
+ else { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
615
+ } catch (e) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Error: ' + e.message); }
615
616
  }
616
617
 
617
618
  async function prdItemRemove(source, itemId) {
618
619
  if (!confirm('Remove item ' + itemId + '? This also cancels any pending work item.')) return;
620
+ closeModal();
621
+ showToast('cmd-toast', 'Item removed', true);
619
622
  try {
620
623
  const res = await fetch('/api/prd-items/remove', {
621
624
  method: 'POST', headers: { 'Content-Type': 'application/json' },
622
625
  body: JSON.stringify({ source, itemId })
623
626
  });
624
- if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Item removed', true); }
625
- else { const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
626
- } catch (e) { alert('Error: ' + e.message); }
627
+ if (res.ok) { refresh(); }
628
+ else { const d = await res.json().catch(() => ({})); alert('Remove failed: ' + (d.error || 'unknown')); refresh(); }
629
+ } catch (e) { alert('Error: ' + e.message); refresh(); }
627
630
  }
628
631
 
629
632
  async function prdItemRequeue(workItemId, source) {
@@ -123,8 +123,9 @@ function openAddPrModal() {
123
123
  }
124
124
 
125
125
  async function _submitLinkPr() {
126
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Linking...'; }
126
127
  const url = document.getElementById('pr-link-url')?.value?.trim();
127
- if (!url) { alert('PR URL is required'); return; }
128
+ if (!url) { if (btn) { btn.disabled = false; btn.textContent = 'Link PR'; } alert('PR URL is required'); return; }
128
129
  const title = document.getElementById('pr-link-title')?.value?.trim() || '';
129
130
  const project = document.getElementById('pr-link-project')?.value || '';
130
131
  const context = document.getElementById('pr-link-context')?.value || '';
@@ -75,7 +75,7 @@ function _generateScheduleId(title) {
75
75
  .replace(/-+/g, '-')
76
76
  .replace(/^-|-$/g, '')
77
77
  .slice(0, 40);
78
- const suffix = Math.random().toString(36).slice(2, 6);
78
+ const suffix = Math.random().toString(36).slice(2, 10);
79
79
  return (slug || 'task') + '-' + suffix;
80
80
  }
81
81
 
@@ -487,6 +487,7 @@ function openEditScheduleModal(id) {
487
487
  }
488
488
 
489
489
  async function submitSchedule(isEdit) {
490
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = isEdit ? 'Saving...' : 'Creating...'; }
490
491
  _showScheduleError('');
491
492
  const title = document.getElementById('sched-edit-title').value.trim();
492
493
  const cron = window._schedComputedCron || '';
@@ -503,8 +504,9 @@ async function submitSchedule(isEdit) {
503
504
  id = _generateScheduleId(title);
504
505
  }
505
506
 
506
- if (!title) { _showScheduleError('Title is required'); return; }
507
- if (!cron) { _showScheduleError('Schedule is required \u2014 select days and time, or use natural language'); return; }
507
+ function _resetSchedBtn() { if (btn) { btn.disabled = false; btn.textContent = isEdit ? 'Save Changes' : 'Create Schedule'; } }
508
+ if (!title) { _resetSchedBtn(); _showScheduleError('Title is required'); return; }
509
+ if (!cron) { _resetSchedBtn(); _showScheduleError('Schedule is required \u2014 select days and time, or use natural language'); return; }
508
510
 
509
511
  const payload = { id, title, cron, type, priority, project: project || undefined, agent: agent || undefined, description: description || undefined, enabled: true };
510
512
  const url = isEdit ? '/api/schedules/update' : '/api/schedules';
@@ -515,9 +517,9 @@ async function submitSchedule(isEdit) {
515
517
  });
516
518
  if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', isEdit ? 'Schedule updated' : 'Schedule created', true); } else {
517
519
  const d = await res.json().catch(() => ({}));
518
- _showScheduleError((isEdit ? 'Update' : 'Create') + ' failed: ' + (d.error || 'unknown'));
520
+ _resetSchedBtn(); _showScheduleError((isEdit ? 'Update' : 'Create') + ' failed: ' + (d.error || 'unknown'));
519
521
  }
520
- } catch (e) { _showScheduleError('Error: ' + e.message); }
522
+ } catch (e) { _resetSchedBtn(); _showScheduleError('Error: ' + e.message); }
521
523
  }
522
524
 
523
525
  async function toggleScheduleEnabled(id, enabled) {
@@ -158,6 +158,7 @@ function editWorkItem(id, source) {
158
158
  }
159
159
 
160
160
  async function submitWorkItemEdit(id, source) {
161
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
161
162
  const title = document.getElementById('wi-edit-title').value.trim();
162
163
  const description = document.getElementById('wi-edit-desc').value;
163
164
  const type = document.getElementById('wi-edit-type').value;
@@ -170,7 +171,7 @@ async function submitWorkItemEdit(id, source) {
170
171
  });
171
172
  const acRaw = document.getElementById('wi-edit-ac')?.value || '';
172
173
  const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
173
- if (!title) { alert('Title is required'); return; }
174
+ if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title is required'); return; }
174
175
  try { closeModal(); } catch { /* may not be open */ }
175
176
  showToast('cmd-toast', 'Work item updated', true);
176
177
  try {
@@ -365,8 +366,9 @@ function openCreateWorkItemModal() {
365
366
  }
366
367
 
367
368
  async function _submitCreateWorkItem() {
369
+ var btn = event?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
368
370
  const title = document.getElementById('wi-new-title')?.value?.trim();
369
- if (!title) { alert('Title is required'); return; }
371
+ if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Create'; } alert('Title is required'); return; }
370
372
  const desc = document.getElementById('wi-new-desc')?.value || '';
371
373
  const type = document.getElementById('wi-new-type')?.value || 'implement';
372
374
  const priority = document.getElementById('wi-new-priority')?.value || 'medium';
@@ -15,6 +15,11 @@ function getPageFromUrl() {
15
15
  let currentPage = getPageFromUrl();
16
16
 
17
17
  function switchPage(page, pushState) {
18
+ // Clean up intervals and panels from previous page
19
+ try { _stopPlanPoll(); } catch {}
20
+ try { _stopMeetingPoll(); } catch {}
21
+ try { closeDetail(); } catch {}
22
+
18
23
  currentPage = page;
19
24
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
20
25
  const target = document.getElementById('page-' + page);
package/engine/ado.js CHANGED
@@ -46,9 +46,20 @@ function getAdoToken() {
46
46
 
47
47
  async function adoFetch(url, token, _retryCount = 0) {
48
48
  const MAX_RETRIES = 1;
49
- const res = await fetch(url, {
50
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
51
- });
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), 30000);
51
+ let res;
52
+ try {
53
+ res = await fetch(url, {
54
+ signal: controller.signal,
55
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
56
+ });
57
+ } catch (err) {
58
+ clearTimeout(timer);
59
+ if (err.name === 'AbortError') throw new Error(`ADO API timeout (30s) for ${url.split('?')[0]}`);
60
+ throw err;
61
+ }
62
+ clearTimeout(timer);
52
63
  if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
53
64
  const text = await res.text();
54
65
  if (!text || text.trimStart().startsWith('<')) {
package/engine/cleanup.js CHANGED
@@ -202,7 +202,7 @@ function runCleanup(config, verbose = false) {
202
202
  for (const entry of wtEntries) {
203
203
  if (entry.shouldClean) {
204
204
  try {
205
- exec(`git worktree remove "${entry.wtPath}" --force`, { cwd: root, stdio: 'pipe' });
205
+ exec(`git worktree remove "${entry.wtPath}" --force`, { cwd: root, stdio: 'pipe', timeout: 30000 });
206
206
  cleaned.worktrees++;
207
207
  if (verbose) console.log(` Removed worktree: ${entry.wtPath}`);
208
208
  } catch (e) {
@@ -817,13 +817,17 @@ async function handlePostMerge(pr, project, config, newStatus) {
817
817
 
818
818
  const teamsUrl = process.env.TEAMS_PLAN_FLOW_URL;
819
819
  if (teamsUrl) {
820
+ const ac = new AbortController();
821
+ const t = setTimeout(() => ac.abort(), 5000);
820
822
  try {
821
823
  await fetch(teamsUrl, {
822
824
  method: 'POST',
825
+ signal: ac.signal,
823
826
  headers: { 'Content-Type': 'application/json' },
824
827
  body: JSON.stringify({ text: `PR ${pr.id} merged: ${pr.title} (${project.name}) by ${pr.agent || 'unknown'}` })
825
828
  });
826
829
  } catch (err) { log('warn', `Teams post-merge notify failed: ${err.message}`); }
830
+ clearTimeout(t);
827
831
  }
828
832
 
829
833
  log('info', `Post-merge hooks completed for ${pr.id}`);
package/engine/meeting.js CHANGED
@@ -44,7 +44,7 @@ function saveMeeting(meeting) {
44
44
  }
45
45
 
46
46
  function createMeeting({ title, agenda, participants }) {
47
- const id = 'MTG-' + uid().slice(0, 8);
47
+ const id = 'MTG-' + uid();
48
48
  const meeting = {
49
49
  id, title, agenda,
50
50
  status: 'investigating',
@@ -169,7 +169,7 @@ function executeTaskStage(stage, stageState, run, config) {
169
169
  id,
170
170
  title: item.title || stage.title,
171
171
  description: item.description || stage.description || '',
172
- type: item.type || stage.taskType || 'implement',
172
+ type: item.type || stage.taskType || 'explore',
173
173
  priority: item.priority || stage.priority || 'medium',
174
174
  agent: item.agent || stage.agent || '',
175
175
  status: 'pending',
package/engine/routing.js CHANGED
@@ -136,7 +136,7 @@ function resolveAgent(workType, config, authorAgent = null) {
136
136
  if (config.engine?.allowTempAgents) {
137
137
  const tempId = `temp-${shared.uid()}`;
138
138
  _claimedAgents.add(tempId);
139
- tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: ts() });
139
+ tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 13)}`, role: 'Temporary Agent', createdAt: ts() });
140
140
  log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
141
141
  return tempId;
142
142
  }
package/engine/shared.js CHANGED
@@ -68,7 +68,15 @@ function safeWrite(p, data) {
68
68
  const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
69
69
  const tmp = p + '.tmp.' + process.pid + '.' + (++_tmpCounter);
70
70
  try {
71
- fs.writeFileSync(tmp, content);
71
+ try {
72
+ fs.writeFileSync(tmp, content);
73
+ } catch (writeErr) {
74
+ if (writeErr.code === 'ENOSPC') {
75
+ try { fs.unlinkSync(tmp); } catch { /* cleanup */ }
76
+ throw new Error(`[ENOSPC] Disk full — cannot write ${path.basename(p)}`);
77
+ }
78
+ throw writeErr;
79
+ }
72
80
  // Atomic rename — retry on Windows EPERM (file locking)
73
81
  for (let attempt = 0; attempt < 5; attempt++) {
74
82
  try {
@@ -168,7 +176,7 @@ function mutateJsonFileLocked(filePath, mutateFn, {
168
176
  * Use for filenames that could collide (dispatch IDs, temp files, etc.)
169
177
  */
170
178
  function uid() {
171
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
179
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
172
180
  }
173
181
 
174
182
  /**
@@ -38,7 +38,7 @@ for (const p of searchPaths) {
38
38
  // Fallback: parse the shell wrapper
39
39
  if (!claudeBin) {
40
40
  try {
41
- const which = exec('bash -c "which claude"', { encoding: 'utf8', env }).trim();
41
+ const which = exec('bash -c "which claude"', { encoding: 'utf8', env, timeout: 10000 }).trim();
42
42
  const whichNative = which.replace(/^\/([a-zA-Z])\//, (_, d) => d.toUpperCase() + ':/').replace(/\//g, path.sep);
43
43
  const wrapper = fs.readFileSync(whichNative, 'utf8');
44
44
  const m = wrapper.match(/node_modules\/@anthropic-ai\/claude-code\/cli\.js/);
package/engine.js CHANGED
@@ -2220,12 +2220,15 @@ let tickRunning = false;
2220
2220
  async function tick() {
2221
2221
  if (tickRunning) return; // prevent overlapping ticks
2222
2222
  tickRunning = true;
2223
+ const tickStart = Date.now();
2223
2224
  try {
2224
2225
  await tickInner();
2225
2226
  } catch (e) {
2226
2227
  log('error', `Tick error: ${e.message}`);
2227
2228
  } finally {
2228
2229
  tickRunning = false;
2230
+ const elapsed = Date.now() - tickStart;
2231
+ if (elapsed > 30000) log('warn', `Slow tick: ${(elapsed / 1000).toFixed(1)}s`);
2229
2232
  }
2230
2233
  }
2231
2234
 
@@ -2243,9 +2246,9 @@ async function tickInner() {
2243
2246
  tickCount++;
2244
2247
 
2245
2248
  // 1. Check for timed-out agents, steering messages, and idle threshold
2246
- checkTimeouts(config);
2247
- checkSteering(config);
2248
- checkIdleThreshold(config);
2249
+ try { checkTimeouts(config); } catch (e) { log('warn', `checkTimeouts: ${e.message}`); }
2250
+ try { checkSteering(config); } catch (e) { log('warn', `checkSteering: ${e.message}`); }
2251
+ try { checkIdleThreshold(config); } catch (e) { log('warn', `checkIdleThreshold: ${e.message}`); }
2249
2252
 
2250
2253
  // 1b. Check for meeting round timeouts
2251
2254
  try {
@@ -2260,11 +2263,11 @@ async function tickInner() {
2260
2263
  }
2261
2264
 
2262
2265
  // 2. Consolidate inbox
2263
- consolidateInbox(config);
2266
+ try { consolidateInbox(config); } catch (e) { log('warn', `consolidateInbox: ${e.message}`); }
2264
2267
 
2265
2268
  // 2.5. Periodic cleanup + MCP sync (every 10 ticks = ~5 minutes)
2266
2269
  if (tickCount % 10 === 0) {
2267
- runCleanup(config);
2270
+ try { runCleanup(config); } catch (e) { log('warn', `runCleanup: ${e.message}`); }
2268
2271
  }
2269
2272
 
2270
2273
  // 2.6. Poll PR status: build, review, merge (every 6 ticks = ~3 minutes)
@@ -2395,14 +2398,15 @@ async function tickInner() {
2395
2398
  }
2396
2399
 
2397
2400
  // 3. Discover new work from sources
2398
- discoverWork(config);
2401
+ try { discoverWork(config); } catch (e) { log('warn', `discoverWork: ${e.message}`); }
2399
2402
 
2400
2403
  // 4. Update snapshot
2401
- updateSnapshot(config);
2404
+ try { updateSnapshot(config); } catch (e) { log('warn', `updateSnapshot: ${e.message}`); }
2402
2405
 
2403
2406
  // 5. Process pending dispatches — auto-spawn agents
2404
- const dispatch = getDispatch();
2405
- const activeCount = (dispatch.active || []).length;
2407
+ let dispatch, activeCount;
2408
+ try { dispatch = getDispatch(); } catch (e) { log('warn', `getDispatch: ${e.message}`); return; }
2409
+ activeCount = (dispatch.active || []).length;
2406
2410
  const maxConcurrent = config.engine?.maxConcurrent || 5;
2407
2411
 
2408
2412
  if (activeCount >= maxConcurrent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.179",
3
+ "version": "0.1.181",
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"