@yemi33/minions 0.1.150 → 0.1.151

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.151 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/lifecycle.js
8
+
9
+ ### Dashboard
10
+ - dashboard/js/command-center.js
11
+ - dashboard/js/render-meetings.js
12
+
13
+ ### Playbooks
14
+ - work-item.md
15
+
3
16
  ## 0.1.150 (2026-04-01)
4
17
 
5
18
  ### Dashboard
@@ -37,6 +37,28 @@ function ccRestoreMessages() {
37
37
  for (const msg of _ccMessages) {
38
38
  ccAddMessage(msg.role, msg.html, true);
39
39
  }
40
+ // Restore "thinking" indicator if CC was mid-request when page refreshed
41
+ try {
42
+ const sendingState = JSON.parse(localStorage.getItem('cc-sending') || 'null');
43
+ if (sendingState?.sending && (Date.now() - sendingState.startedAt) < 300000) {
44
+ _ccSending = true;
45
+ const elapsed = Date.now() - sendingState.startedAt;
46
+ const thinking = document.createElement('div');
47
+ thinking.id = 'cc-thinking';
48
+ thinking.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:11px;color:var(--muted);align-self:flex-start;display:flex;align-items:center;gap:8px';
49
+ thinking.innerHTML = '<span class="dot-pulse" style="display:inline-flex;gap:3px"><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.2s"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.4s"></span></span> <span id="cc-thinking-text">Still working...</span> <span id="cc-thinking-time" style="font-size:10px;color:var(--border)">' + Math.floor(elapsed / 1000) + 's</span>' +
50
+ ' <button onclick="ccNewSession()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Reset</button>';
51
+ el.appendChild(thinking);
52
+ el.scrollTop = el.scrollHeight;
53
+ // Update timer
54
+ const startTime = sendingState.startedAt;
55
+ const restoreTimer = setInterval(function() {
56
+ var timeEl = document.getElementById('cc-thinking-time');
57
+ if (!timeEl || !_ccSending) { clearInterval(restoreTimer); return; }
58
+ timeEl.textContent = Math.floor((Date.now() - startTime) / 1000) + 's';
59
+ }, 1000);
60
+ }
61
+ } catch {}
40
62
  }
41
63
 
42
64
  function ccSaveState() {
@@ -109,6 +131,7 @@ async function ccSend() {
109
131
 
110
132
  async function _ccDoSend(message, skipUserMsg) {
111
133
  _ccSending = true;
134
+ try { localStorage.setItem('cc-sending', JSON.stringify({ sending: true, startedAt: Date.now() })); } catch {}
112
135
 
113
136
  if (!skipUserMsg) ccAddMessage('user', escHtml(message));
114
137
 
@@ -160,7 +183,9 @@ async function _ccDoSend(message, skipUserMsg) {
160
183
  thinking.remove();
161
184
 
162
185
  if (data.error) {
163
- ccAddMessage('assistant', '<span style="color:var(--red)">' + escHtml(data.error) + '</span>');
186
+ const isBusy = data.error.includes('busy');
187
+ ccAddMessage('assistant', '<span style="color:var(--red)">' + escHtml(data.error) + '</span>' +
188
+ (isBusy ? ' <button onclick="ccNewSession()" style="margin-top:4px;padding:3px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:10px">Reset CC</button>' : ''));
164
189
  return;
165
190
  }
166
191
 
@@ -201,6 +226,7 @@ async function _ccDoSend(message, skipUserMsg) {
201
226
  '<button id="' + retryId + '" onclick="ccRetryLast()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:11px">Retry</button>');
202
227
  } finally {
203
228
  _ccSending = false;
229
+ try { localStorage.removeItem('cc-sending'); } catch {}
204
230
  // Show notification badge on CC button if drawer is closed
205
231
  if (!_ccOpen) showNotifBadge(document.getElementById('cc-toggle-btn'));
206
232
  }
@@ -173,11 +173,11 @@ function _renderMeetingDetail(m) {
173
173
  } else {
174
174
  html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
175
175
  '<input id="meeting-note-input" type="text" placeholder="Add context for all agents..." style="flex:1;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:12px" onkeydown="if(event.key===\'Enter\')_submitMeetingNote(\'' + escHtml(m.id) + '\')">' +
176
- '<button onclick="_submitMeetingNote(\'' + escHtml(m.id) + '\')" style="padding:6px 12px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:11px">Add Note</button>' +
176
+ '<button onclick="_submitMeetingNote(\'' + escHtml(m.id) + '\',this)" style="padding:6px 12px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:11px">Add Note</button>' +
177
177
  '</div>' +
178
178
  '<div style="display:flex;gap:8px;margin-top:4px">' +
179
- '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\')">Skip to Next Round</button>' +
180
- '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_endMeeting(\'' + escHtml(m.id) + '\')">End Meeting</button>' +
179
+ '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\',this)">Skip to Next Round</button>' +
180
+ '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_endMeeting(\'' + escHtml(m.id) + '\',this)">End Meeting</button>' +
181
181
  '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
182
182
  '</div>';
183
183
  }
@@ -269,45 +269,58 @@ async function _submitCreateMeeting() {
269
269
  } catch (e) { alert('Error: ' + e.message); openCreateMeetingModal(); }
270
270
  }
271
271
 
272
- async function _submitMeetingNote(id) {
272
+ async function _submitMeetingNote(id, btn) {
273
273
  const input = document.getElementById('meeting-note-input');
274
274
  if (!input?.value?.trim()) return;
275
275
  const note = input.value.trim();
276
276
  input.value = '';
277
+ if (btn) { btn.textContent = 'Adding...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
277
278
  try {
278
279
  const res = await fetch('/api/meetings/note', {
279
280
  method: 'POST', headers: { 'Content-Type': 'application/json' },
280
281
  body: JSON.stringify({ id, note })
281
282
  });
282
- if (res.ok) openMeetingDetail(id);
283
+ if (res.ok) { showToast('cmd-toast', 'Note added', true); }
283
284
  else { input.value = note; alert('Failed to add note'); }
284
285
  } catch (e) { input.value = note; alert('Error: ' + e.message); }
286
+ if (btn) { btn.textContent = 'Add Note'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
285
287
  }
286
288
 
287
- async function _advanceMeeting(id) {
289
+ async function _advanceMeeting(id, btn) {
288
290
  if (!confirm('Skip to next round? Agents that haven\'t finished will be skipped.')) return;
289
- showToast('cmd-toast', 'Advancing to next round...', true);
291
+ if (btn) { btn.textContent = 'Advancing...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
290
292
  try {
291
- await fetch('/api/meetings/advance', {
293
+ const res = await fetch('/api/meetings/advance', {
292
294
  method: 'POST', headers: { 'Content-Type': 'application/json' },
293
295
  body: JSON.stringify({ id })
294
296
  });
295
- wakeEngine();
296
- openMeetingDetail(id);
297
+ if (res.ok) {
298
+ showToast('cmd-toast', 'Advanced to next round', true);
299
+ wakeEngine();
300
+ } else {
301
+ const d = await res.json().catch(function() { return {}; });
302
+ alert('Advance failed: ' + (d.error || 'unknown'));
303
+ }
297
304
  } catch (e) { alert('Error: ' + e.message); }
305
+ if (btn) { btn.textContent = 'Skip to Next Round'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
298
306
  }
299
307
 
300
- async function _endMeeting(id) {
308
+ async function _endMeeting(id, btn) {
301
309
  if (!confirm('End this meeting? Current round will be stopped.')) return;
302
- try { closeModal(); } catch { /* expected */ }
303
- showToast('cmd-toast', 'Meeting ended', true);
310
+ if (btn) { btn.textContent = 'Ending...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
304
311
  try {
305
- await fetch('/api/meetings/end', {
312
+ const res = await fetch('/api/meetings/end', {
306
313
  method: 'POST', headers: { 'Content-Type': 'application/json' },
307
314
  body: JSON.stringify({ id })
308
315
  });
309
- refresh();
310
- } catch (e) { alert('Error: ' + e.message); refresh(); }
316
+ if (res.ok) {
317
+ showToast('cmd-toast', 'Meeting ended', true);
318
+ } else {
319
+ const d = await res.json().catch(function() { return {}; });
320
+ alert('End failed: ' + (d.error || 'unknown'));
321
+ if (btn) { btn.textContent = 'End Meeting'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
322
+ }
323
+ } catch (e) { alert('Error: ' + e.message); if (btn) { btn.textContent = 'End Meeting'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
311
324
  }
312
325
 
313
326
  async function _archiveMeeting(id) {
@@ -1221,24 +1221,24 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1221
1221
 
1222
1222
  if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
1223
1223
 
1224
- // Auto-dispatch evaluate work item after implement completes successfully
1224
+ // Auto-dispatch review work item after implement completes successfully
1225
1225
  if (isSuccess && !skipDoneStatus && type === 'implement' && meta?.item?.id) {
1226
- const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
1227
- if (evalLoop) {
1226
+ const autoReview = config.engine?.autoReview ?? shared.ENGINE_DEFAULTS.autoReview;
1227
+ if (autoReview) {
1228
1228
  try {
1229
1229
  const wiPath = resolveWiPath(meta);
1230
1230
  if (wiPath) {
1231
1231
  const items = safeJson(wiPath) || [];
1232
- // Dedup: skip if an evaluate item already exists for this parent
1233
- const existing = items.find(i => i._evalParentId === meta.item.id && i.type === 'evaluate');
1232
+ // Dedup: skip if a review item already exists for this parent
1233
+ const existing = items.find(i => i._evalParentId === meta.item.id && i.type === 'review');
1234
1234
  if (existing) {
1235
- log('info', `Eval loop: evaluate item ${existing.id} already exists for ${meta.item.id}, skipping`);
1235
+ log('info', `Eval loop: review item ${existing.id} already exists for ${meta.item.id}, skipping`);
1236
1236
  } else {
1237
1237
  const parentItem = items.find(i => i.id === meta.item.id);
1238
1238
  const evalItem = {
1239
1239
  id: 'W-' + shared.uid(),
1240
- title: `Evaluate: ${meta.item.title || meta.item.id}`,
1241
- type: 'evaluate',
1240
+ title: `Review: ${meta.item.title || meta.item.id}`,
1241
+ type: 'review',
1242
1242
  priority: meta.item.priority || 'high',
1243
1243
  status: 'pending',
1244
1244
  created: ts(),
@@ -1263,14 +1263,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1263
1263
  }
1264
1264
  }
1265
1265
 
1266
- // Evaluate completion: parse verdict and handle eval→fix iteration loop
1267
- if (isSuccess && type === 'evaluate' && meta?.item?._evalParentId) {
1266
+ // Review completion: parse verdict and handle review→fix iteration loop
1267
+ if (isSuccess && type === 'review' && meta?.item?._evalParentId) {
1268
1268
  try {
1269
1269
  const verdict = parseEvalVerdict(resultSummary || stdout);
1270
- const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
1270
+ const autoReview = config.engine?.autoReview ?? shared.ENGINE_DEFAULTS.autoReview;
1271
1271
  const maxIter = config.engine?.evalMaxIterations ?? shared.ENGINE_DEFAULTS.evalMaxIterations;
1272
1272
 
1273
- if (verdict && !verdict.pass && evalLoop) {
1273
+ if (verdict && !verdict.pass && autoReview) {
1274
1274
  const wiPath = resolveWiPath(meta);
1275
1275
  if (wiPath) {
1276
1276
  const items = safeJson(wiPath) || [];
package/engine.js CHANGED
@@ -1256,8 +1256,8 @@ function discoverFromPrs(config, project) {
1256
1256
  if (!agentId) continue;
1257
1257
 
1258
1258
  const item = buildPrDispatch(agentId, config, project, pr, 'review', {
1259
- pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
1260
- pr_author: pr.agent || '', pr_url: pr.url || '',
1259
+ pr_id: pr.id, pr_number: prNumber, pr_title: pr.title ? ': ' + pr.title : '', pr_branch: pr.branch || '',
1260
+ branch_name: pr.branch || '', pr_author: pr.agent || '', pr_url: pr.url || '',
1261
1261
  }, `Review PR ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
1262
1262
  if (item) { newWork.push(item); setCooldown(key); }
1263
1263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.150",
3
+ "version": "0.1.151",
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"
@@ -40,7 +40,7 @@ Keep branch names lowercase, use hyphens, max 60 chars.
40
40
  - title: `feat({{item_id}}): <description>`
41
41
  8. **Post implementation notes** as a PR thread comment:
42
42
  {{pr_comment_instructions}}
43
- 9. **Add PR to tracker** — append to `{{project_path}}/.minions/pull-requests.json`:
43
+ 9. **Add PR to tracker** — append to `{{team_root}}/projects/{{project_name}}/pull-requests.json`:
44
44
  ```json
45
45
  { "id": "PR-<number>", "title": "...", "agent": "{{agent_name}}", "branch": "...", "reviewStatus": "pending", "status": "active", "created": "<date>", "url": "<pr-url>", "prdItems": ["{{item_id}}"] }
46
46
  ```