@yemi33/minions 0.1.531 → 0.1.532

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.532 (2026-04-07)
4
+
5
+ ### Fixes
6
+ - meeting cards show doc-chat processing dots and notification badges
7
+
3
8
  ## 0.1.531 (2026-04-07)
4
9
 
5
10
  ### Fixes
@@ -48,7 +48,7 @@ function renderMeetings(meetings) {
48
48
  const dt = m.completedAt || m.createdAt;
49
49
  const timeStr = dt ? new Date(dt).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
50
50
 
51
- return '<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer" onclick="openMeetingDetail(\'' + escHtml(m.id) + '\')">' +
51
+ return '<div data-file="meetings/' + escHtml(m.id) + '.json" style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer;position:relative" onclick="openMeetingDetail(\'' + escHtml(m.id) + '\')">' +
52
52
  '<div style="display:flex;justify-content:space-between;align-items:center">' +
53
53
  '<strong style="font-size:13px">' + escHtml(m.title) + '</strong>' +
54
54
  '<div style="display:flex;align-items:center;gap:8px">' +
@@ -78,6 +78,7 @@ function renderMeetings(meetings) {
78
78
  el.innerHTML += '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">' +
79
79
  (_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>';
80
80
  }
81
+ restoreNotifBadges();
81
82
  }
82
83
 
83
84
  function _mtgPrev() { if (_mtgPage > 0) { _mtgPage--; refresh(); } }
package/dashboard.js CHANGED
@@ -22,7 +22,7 @@ const shared = require('./engine/shared');
22
22
  const queries = require('./engine/queries');
23
23
  const os = require('os');
24
24
 
25
- const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects, DONE_STATUSES, WI_STATUS } = shared;
25
+ const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS } = shared;
26
26
  const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
27
27
  getSkills, getInbox, getNotesWithMeta, getPullRequests,
28
28
  getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
@@ -1381,9 +1381,6 @@ const server = http.createServer(async (req, res) => {
1381
1381
  if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
1382
1382
  // Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
1383
1383
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
1384
- let items = [];
1385
- const existing = safeRead(wiPath);
1386
- if (existing) { try { items = JSON.parse(existing); } catch {} }
1387
1384
  const id = 'W-' + shared.uid();
1388
1385
  const item = {
1389
1386
  id, title: body.title, type: 'plan',
@@ -1393,8 +1390,7 @@ const server = http.createServer(async (req, res) => {
1393
1390
  };
1394
1391
  if (body.project) item.project = body.project;
1395
1392
  if (body.agent) item.agent = body.agent;
1396
- items.push(item);
1397
- safeWrite(wiPath, items);
1393
+ mutateWorkItems(wiPath, items => { items.push(item); });
1398
1394
  return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
1399
1395
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1400
1396
  }
@@ -1465,18 +1461,18 @@ const server = http.createServer(async (req, res) => {
1465
1461
  }
1466
1462
  for (const wiPath of wiSyncPaths) {
1467
1463
  try {
1468
- const items = safeJson(wiPath);
1469
- const wi = items.find(w => w.sourcePlan === body.source && w.id === body.itemId);
1470
- if (wi && wi.status === 'pending') {
1471
- if (body.name !== undefined) wi.title = 'Implement: ' + body.name;
1472
- if (body.description !== undefined) wi.description = body.description;
1473
- if (body.priority !== undefined) wi.priority = body.priority;
1474
- if (body.estimated_complexity !== undefined) {
1475
- wi.type = body.estimated_complexity === 'large' ? 'implement:large' : 'implement';
1464
+ mutateWorkItems(wiPath, items => {
1465
+ const wi = items.find(w => w.sourcePlan === body.source && w.id === body.itemId);
1466
+ if (wi && wi.status === 'pending') {
1467
+ if (body.name !== undefined) wi.title = 'Implement: ' + body.name;
1468
+ if (body.description !== undefined) wi.description = body.description;
1469
+ if (body.priority !== undefined) wi.priority = body.priority;
1470
+ if (body.estimated_complexity !== undefined) {
1471
+ wi.type = body.estimated_complexity === 'large' ? 'implement:large' : 'implement';
1472
+ }
1473
+ workItemSynced = true;
1476
1474
  }
1477
- safeWrite(wiPath, items);
1478
- workItemSynced = true;
1479
- }
1475
+ });
1480
1476
  } catch (e) { console.error('work item sync:', e.message); }
1481
1477
  }
1482
1478
 
@@ -1500,26 +1496,21 @@ const server = http.createServer(async (req, res) => {
1500
1496
 
1501
1497
  // Also remove any materialized work item for this plan item
1502
1498
  let cancelled = false;
1499
+ const allWiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
1503
1500
  for (const proj of PROJECTS) {
1504
- const wiPath = shared.projectWorkItemsPath(proj);
1501
+ allWiPaths.push(shared.projectWorkItemsPath(proj));
1502
+ }
1503
+ for (const wiPath of allWiPaths) {
1505
1504
  try {
1506
- const items = safeJson(wiPath);
1507
- const before = items.length;
1508
- const filtered = items.filter(w => !(w.sourcePlan === body.source && w.id === body.itemId));
1509
- if (filtered.length < before) {
1510
- safeWrite(wiPath, filtered);
1511
- cancelled = true;
1512
- }
1505
+ mutateWorkItems(wiPath, items => {
1506
+ const filtered = items.filter(w => !(w.sourcePlan === body.source && w.id === body.itemId));
1507
+ if (filtered.length < items.length) {
1508
+ cancelled = true;
1509
+ return filtered;
1510
+ }
1511
+ });
1513
1512
  } catch (e) { console.error('work item cleanup:', e.message); }
1514
1513
  }
1515
- // Also check central work-items
1516
- const centralPath = path.join(MINIONS_DIR, 'work-items.json');
1517
- try {
1518
- const items = safeJson(centralPath);
1519
- const before = items.length;
1520
- const filtered = items.filter(w => !(w.sourcePlan === body.source && w.id === body.itemId));
1521
- if (filtered.length < before) { safeWrite(centralPath, filtered); cancelled = true; }
1522
- } catch (e) { console.error('central work item cleanup:', e.message); }
1523
1514
 
1524
1515
  // Clean dispatch entries for this item
1525
1516
  cleanDispatchEntries(d =>
@@ -2130,52 +2121,51 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2130
2121
  mutateJsonFileLocked(dispatchPath, (dispatch) => {
2131
2122
  for (const wiPath of wiPaths) {
2132
2123
  try {
2133
- const items = safeJson(wiPath);
2134
- if (!items) continue;
2135
- let changed = false;
2136
- for (const w of items) {
2137
- if (w.sourcePlan !== body.file) continue;
2138
- // Keep completed items as-is, reset everything else to pending.
2139
- if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
2140
-
2141
- if (w.status === WI_STATUS.DISPATCHED) {
2142
- // Kill the agent working on this item, if any.
2143
- const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
2144
- if (activeEntry) {
2145
- const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
2146
- try {
2147
- const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
2148
- if (agentStatus.pid) {
2149
- try {
2150
- const safePid = shared.validatePid(agentStatus.pid);
2151
- if (process.platform === 'win32') {
2152
- require('child_process').execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
2153
- } else {
2154
- process.kill(safePid, 'SIGTERM');
2155
- }
2156
- } catch { /* process may be dead or invalid PID */ }
2157
- }
2158
- agentStatus.status = 'idle';
2159
- delete agentStatus.currentTask;
2160
- delete agentStatus.dispatched;
2161
- safeWrite(statusPath, agentStatus);
2162
- } catch (e) { console.error('agent reset:', e.message); }
2163
- killedAgents.add(activeEntry.agent);
2124
+ mutateWorkItems(wiPath, items => {
2125
+ let changed = false;
2126
+ for (const w of items) {
2127
+ if (w.sourcePlan !== body.file) continue;
2128
+ // Keep completed items as-is, reset everything else to pending.
2129
+ if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
2130
+
2131
+ if (w.status === WI_STATUS.DISPATCHED) {
2132
+ // Kill the agent working on this item, if any.
2133
+ const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
2134
+ if (activeEntry) {
2135
+ const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
2136
+ try {
2137
+ const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
2138
+ if (agentStatus.pid) {
2139
+ try {
2140
+ const safePid = shared.validatePid(agentStatus.pid);
2141
+ if (process.platform === 'win32') {
2142
+ require('child_process').execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
2143
+ } else {
2144
+ process.kill(safePid, 'SIGTERM');
2145
+ }
2146
+ } catch { /* process may be dead or invalid PID */ }
2147
+ }
2148
+ agentStatus.status = 'idle';
2149
+ delete agentStatus.currentTask;
2150
+ delete agentStatus.dispatched;
2151
+ safeWrite(statusPath, agentStatus);
2152
+ } catch (e) { console.error('agent reset:', e.message); }
2153
+ killedAgents.add(activeEntry.agent);
2154
+ }
2164
2155
  }
2165
- }
2166
2156
 
2167
- if (w.status !== WI_STATUS.PAUSED) reset++;
2168
- w.status = WI_STATUS.PAUSED;
2169
- w._pausedBy = 'prd-pause';
2170
- delete w._resumedAt;
2171
- delete w.dispatched_at;
2172
- delete w.dispatched_to;
2173
- delete w.failReason;
2174
- delete w.failedAt;
2175
- changed = true;
2176
- if (w.id) resetItemIds.add(w.id);
2177
- }
2178
- if (changed) safeWrite(wiPath, items);
2157
+ if (w.status !== WI_STATUS.PAUSED) reset++;
2158
+ w.status = WI_STATUS.PAUSED;
2159
+ w._pausedBy = 'prd-pause';
2160
+ delete w._resumedAt;
2161
+ delete w.dispatched_at;
2162
+ delete w.dispatched_to;
2163
+ delete w.failReason;
2164
+ delete w.failedAt;
2165
+ changed = true;
2166
+ if (w.id) resetItemIds.add(w.id);
2167
+ }
2168
+ });
2179
2169
  } catch (e) { console.error('reset work items:', e.message); }
2180
2170
  }
2181
2171
 
@@ -2220,13 +2210,15 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2220
2210
  const config = queries.getConfig();
2221
2211
  for (const p of getProjects(config)) {
2222
2212
  const projWiPath = projectWorkItemsPath(p);
2223
- const projItems = safeJson(projWiPath);
2224
- if (!projItems) continue;
2225
- const filtered = projItems.filter(w => {
2226
- if (w.sourcePlan !== body.file) return true; // different plan, keep
2227
- return completedStatuses.has(w.status); // keep completed, remove pending/failed
2228
- });
2229
- if (filtered.length < projItems.length) safeWrite(projWiPath, filtered);
2213
+ try {
2214
+ mutateWorkItems(projWiPath, items => {
2215
+ const filtered = items.filter(w => {
2216
+ if (w.sourcePlan !== body.file) return true; // different plan, keep
2217
+ return completedStatuses.has(w.status); // keep completed, remove pending/failed
2218
+ });
2219
+ if (filtered.length < items.length) return filtered;
2220
+ });
2221
+ } catch { /* project may not have work items */ }
2230
2222
  }
2231
2223
 
2232
2224
  // Delete old PRD — agent will write replacement at same path
@@ -2234,30 +2226,29 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2234
2226
 
2235
2227
  // Queue plan-to-prd regeneration with instructions to preserve completed items
2236
2228
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
2237
- let items = [];
2238
- const existing = safeRead(wiPath);
2239
- if (existing) { try { items = JSON.parse(existing); } catch {} }
2240
-
2241
- // Dedup: check if already queued
2242
- const alreadyQueued = items.find(w =>
2243
- w.type === 'plan-to-prd' && w.planFile === plan.source_plan && (w.status === 'pending' || w.status === 'dispatched')
2244
- );
2245
- if (alreadyQueued) return jsonReply(res, 200, { id: alreadyQueued.id, alreadyQueued: true });
2246
2229
 
2247
2230
  const completedContext = completedItems.length > 0
2248
2231
  ? `\n\n**Previously completed items (preserve their status in the new PRD):**\n${completedItems.map(i => `- ${i.id}: ${i.name} [${i.status}]`).join('\n')}`
2249
2232
  : '';
2250
2233
 
2251
2234
  const id = 'W-' + shared.uid();
2252
- items.push({
2253
- id, title: `Regenerate PRD: ${plan.plan_summary || plan.source_plan}`,
2254
- type: 'plan-to-prd', priority: 'high',
2255
- description: `Plan file: plans/${plan.source_plan}\nTarget PRD filename: ${body.file}\nRegeneration requested by user after plan revision.${completedContext}`,
2256
- status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard:regenerate',
2257
- project: plan.project || '', planFile: plan.source_plan,
2258
- _targetPrdFile: body.file,
2235
+ let alreadyQueuedId = null;
2236
+ mutateWorkItems(wiPath, items => {
2237
+ // Dedup: check if already queued
2238
+ const alreadyQueued = items.find(w =>
2239
+ w.type === 'plan-to-prd' && w.planFile === plan.source_plan && (w.status === 'pending' || w.status === 'dispatched')
2240
+ );
2241
+ if (alreadyQueued) { alreadyQueuedId = alreadyQueued.id; return; }
2242
+ items.push({
2243
+ id, title: `Regenerate PRD: ${plan.plan_summary || plan.source_plan}`,
2244
+ type: 'plan-to-prd', priority: 'high',
2245
+ description: `Plan file: plans/${plan.source_plan}\nTarget PRD filename: ${body.file}\nRegeneration requested by user after plan revision.${completedContext}`,
2246
+ status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard:regenerate',
2247
+ project: plan.project || '', planFile: plan.source_plan,
2248
+ _targetPrdFile: body.file,
2249
+ });
2259
2250
  });
2260
- safeWrite(wiPath, items);
2251
+ if (alreadyQueuedId) return jsonReply(res, 200, { id: alreadyQueuedId, alreadyQueued: true });
2261
2252
  return jsonReply(res, 200, { id, file: plan.source_plan });
2262
2253
  } catch (e) { return jsonReply(res, 500, { error: e.message }); }
2263
2254
  }
@@ -2334,27 +2325,26 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2334
2325
 
2335
2326
  for (const wiInfo of wiPaths) {
2336
2327
  try {
2337
- const items = safeJson(wiInfo.path);
2338
- const filtered = [];
2339
- for (const w of items) {
2340
- if (w.sourcePlan === body.source) {
2341
- materializedPlanItemIds.add(w.id);
2342
- if (w.status === 'pending' || w.status === 'failed') {
2343
- // Delete — will re-materialize on next tick with updated plan data
2344
- reset++;
2345
- deletedItemIds.push(w.id);
2328
+ mutateWorkItems(wiInfo.path, items => {
2329
+ const filtered = [];
2330
+ for (const w of items) {
2331
+ if (w.sourcePlan === body.source) {
2332
+ materializedPlanItemIds.add(w.id);
2333
+ if (w.status === 'pending' || w.status === 'failed') {
2334
+ // Delete — will re-materialize on next tick with updated plan data
2335
+ reset++;
2336
+ deletedItemIds.push(w.id);
2337
+ } else {
2338
+ // dispatched or done — leave alone
2339
+ kept++;
2340
+ filtered.push(w);
2341
+ }
2346
2342
  } else {
2347
- // dispatched or done — leave alone
2348
- kept++;
2349
2343
  filtered.push(w);
2350
2344
  }
2351
- } else {
2352
- filtered.push(w);
2353
2345
  }
2354
- }
2355
- if (filtered.length < items.length) {
2356
- safeWrite(wiInfo.path, filtered);
2357
- }
2346
+ if (filtered.length < items.length) return filtered;
2347
+ });
2358
2348
  } catch (e) { console.error('work item sync:', e.message); }
2359
2349
  }
2360
2350
 
@@ -2396,13 +2386,13 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2396
2386
  }
2397
2387
  for (const wiPath of wiPaths) {
2398
2388
  try {
2399
- const items = safeJsonArr(wiPath);
2400
- if (!items) continue;
2401
- const filtered = items.filter(w => w.sourcePlan !== body.file);
2402
- if (filtered.length < items.length) {
2403
- cleaned += items.length - filtered.length;
2404
- safeWrite(wiPath, filtered);
2405
- }
2389
+ mutateWorkItems(wiPath, items => {
2390
+ const filtered = items.filter(w => w.sourcePlan !== body.file);
2391
+ if (filtered.length < items.length) {
2392
+ cleaned += items.length - filtered.length;
2393
+ return filtered;
2394
+ }
2395
+ });
2406
2396
  } catch (e) { console.error('plan cleanup:', e.message); }
2407
2397
  }
2408
2398
 
@@ -2417,16 +2407,14 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2417
2407
  if (prdSourcePlan) {
2418
2408
  try {
2419
2409
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
2420
- const centralItems = safeJson(centralPath) || [];
2421
- let changed = false;
2422
- for (const w of centralItems) {
2423
- if (w.type === 'plan-to-prd' && w.status === 'done' && w.planFile === prdSourcePlan) {
2424
- w.status = 'cancelled';
2425
- w._cancelledBy = 'prd-deleted';
2426
- changed = true;
2410
+ mutateWorkItems(centralPath, items => {
2411
+ for (const w of items) {
2412
+ if (w.type === 'plan-to-prd' && w.status === 'done' && w.planFile === prdSourcePlan) {
2413
+ w.status = 'cancelled';
2414
+ w._cancelledBy = 'prd-deleted';
2415
+ }
2427
2416
  }
2428
- }
2429
- if (changed) safeWrite(centralPath, centralItems);
2417
+ });
2430
2418
  } catch (e) { console.error('plan-to-prd cleanup:', e.message); }
2431
2419
  }
2432
2420
 
@@ -2521,19 +2509,17 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2521
2509
 
2522
2510
  // Create a work item to revise the plan
2523
2511
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
2524
- let items = [];
2525
- const existing = safeRead(wiPath);
2526
- if (existing) { try { items = JSON.parse(existing); } catch {} }
2527
2512
  const id = 'W-' + shared.uid();
2528
- items.push({
2529
- id, title: 'Revise plan: ' + (plan.plan_summary || body.file),
2530
- type: 'plan-to-prd', priority: 'high',
2531
- description: 'Revision requested on plan file: ' + (body.file.endsWith('.json') ? 'prd/' : 'plans/') + body.file + '\n\nFeedback:\n' + body.feedback + '\n\nRevise the plan to address this feedback. Read the existing plan, apply the feedback, and overwrite the file with the updated version. Set status back to "awaiting-approval".',
2532
- status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard:revision',
2533
- project: plan.project || '',
2534
- planFile: body.file,
2513
+ mutateWorkItems(wiPath, items => {
2514
+ items.push({
2515
+ id, title: 'Revise plan: ' + (plan.plan_summary || body.file),
2516
+ type: 'plan-to-prd', priority: 'high',
2517
+ description: 'Revision requested on plan file: ' + (body.file.endsWith('.json') ? 'prd/' : 'plans/') + body.file + '\n\nFeedback:\n' + body.feedback + '\n\nRevise the plan to address this feedback. Read the existing plan, apply the feedback, and overwrite the file with the updated version. Set status back to "awaiting-approval".',
2518
+ status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard:revision',
2519
+ project: plan.project || '',
2520
+ planFile: body.file,
2521
+ });
2535
2522
  });
2536
- safeWrite(wiPath, items);
2537
2523
  return jsonReply(res, 200, { ok: true, status: 'revision-requested', workItemId: id });
2538
2524
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
2539
2525
  }
@@ -2624,22 +2610,23 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2624
2610
  const deletedItemIds = [];
2625
2611
  for (const wiInfo of wiPaths) {
2626
2612
  try {
2627
- const items = safeJson(wiInfo.path);
2628
- const filtered = [];
2629
- for (const w of items) {
2630
- if (w.sourcePlan === body.source) {
2631
- if (w.status === 'pending' || w.status === 'failed') {
2632
- reset++;
2633
- deletedItemIds.push(w.id);
2613
+ mutateWorkItems(wiInfo.path, items => {
2614
+ const filtered = [];
2615
+ for (const w of items) {
2616
+ if (w.sourcePlan === body.source) {
2617
+ if (w.status === 'pending' || w.status === 'failed') {
2618
+ reset++;
2619
+ deletedItemIds.push(w.id);
2620
+ } else {
2621
+ kept++;
2622
+ filtered.push(w);
2623
+ }
2634
2624
  } else {
2635
- kept++;
2636
2625
  filtered.push(w);
2637
2626
  }
2638
- } else {
2639
- filtered.push(w);
2640
2627
  }
2641
- }
2642
- if (filtered.length < items.length) safeWrite(wiInfo.path, filtered);
2628
+ if (filtered.length < items.length) return filtered;
2629
+ });
2643
2630
  } catch (e) { console.error('work item deletion:', e.message); }
2644
2631
  }
2645
2632
  for (const itemId of deletedItemIds) {
@@ -2650,22 +2637,21 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2650
2637
 
2651
2638
  // Step 4: Dispatch plan-to-prd to regenerate PRD from revised plan
2652
2639
  const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
2653
- let centralItems = [];
2654
- try { centralItems = JSON.parse(safeRead(centralWiPath) || '[]'); } catch {}
2655
2640
  const wiId = 'W-' + shared.uid();
2656
- centralItems.push({
2657
- id: wiId,
2658
- title: 'Regenerate PRD from revised plan: ' + sourcePlanFile,
2659
- type: 'plan-to-prd',
2660
- priority: 'high',
2661
- description: `The source plan \`${sourcePlanFile}\` has been revised. Convert it into a fresh PRD JSON.\n\nRevision instruction: ${body.instruction}\n\nRead the revised plan, generate updated PRD items (missing_features), and write to \`prd/${body.source}\`. Set status to "approved". Include \`"source_plan": "${sourcePlanFile}"\` in the JSON root.\n\nPreserve items that are already done (status "implemented" or "complete"). Reset or replace items that were pending/failed.`,
2662
- status: 'pending',
2663
- created: new Date().toISOString(),
2664
- createdBy: 'dashboard:revise-and-regenerate',
2665
- project: prd.project || '',
2666
- planFile: sourcePlanFile,
2641
+ mutateWorkItems(centralWiPath, items => {
2642
+ items.push({
2643
+ id: wiId,
2644
+ title: 'Regenerate PRD from revised plan: ' + sourcePlanFile,
2645
+ type: 'plan-to-prd',
2646
+ priority: 'high',
2647
+ description: `The source plan \`${sourcePlanFile}\` has been revised. Convert it into a fresh PRD JSON.\n\nRevision instruction: ${body.instruction}\n\nRead the revised plan, generate updated PRD items (missing_features), and write to \`prd/${body.source}\`. Set status to "approved". Include \`"source_plan": "${sourcePlanFile}"\` in the JSON root.\n\nPreserve items that are already done (status "implemented" or "complete"). Reset or replace items that were pending/failed.`,
2648
+ status: 'pending',
2649
+ created: new Date().toISOString(),
2650
+ createdBy: 'dashboard:revise-and-regenerate',
2651
+ project: prd.project || '',
2652
+ planFile: sourcePlanFile,
2653
+ });
2667
2654
  });
2668
- safeWrite(centralWiPath, centralItems);
2669
2655
 
2670
2656
  return jsonReply(res, 200, {
2671
2657
  ok: true,
@@ -3728,24 +3714,27 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3728
3714
  const projects = shared.getProjects(CONFIG);
3729
3715
  const paths = [path.join(MINIONS_DIR, 'work-items.json')];
3730
3716
  for (const p of projects) paths.push(shared.projectWorkItemsPath(p));
3717
+ let found = null;
3731
3718
  for (const wiPath of paths) {
3732
- const items = JSON.parse(safeRead(wiPath) || '[]');
3733
- const item = items.find(i => i.id === id);
3734
- if (!item) continue;
3735
- item._humanFeedback = { rating, comment: comment || '', at: new Date().toISOString() };
3736
- safeWrite(wiPath, items);
3737
- const agent = item.dispatched_to || item.agent || 'unknown';
3738
- const feedbackNote = '# Human Feedback on ' + id + '\n\n' +
3739
- '**Rating:** ' + (rating === 'up' ? '👍 Good' : '👎 Needs improvement') + '\n' +
3740
- '**Item:** ' + (item.title || id) + '\n' +
3741
- '**Agent:** ' + agent + '\n' +
3742
- (comment ? '**Feedback:** ' + comment + '\n' : '');
3743
- const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', agent + '-feedback-' + new Date().toISOString().slice(0, 10) + '-' + shared.uid().slice(0, 4) + '.md');
3744
- safeWrite(inboxPath, feedbackNote);
3745
- invalidateStatusCache();
3746
- return jsonReply(res, 200, { ok: true });
3719
+ mutateWorkItems(wiPath, items => {
3720
+ const item = items.find(i => i.id === id);
3721
+ if (item && !found) {
3722
+ item._humanFeedback = { rating, comment: comment || '', at: new Date().toISOString() };
3723
+ found = { agent: item.dispatched_to || item.agent || 'unknown', title: item.title || id };
3724
+ }
3725
+ });
3726
+ if (found) break;
3747
3727
  }
3748
- return jsonReply(res, 404, { error: 'Work item not found' });
3728
+ if (!found) return jsonReply(res, 404, { error: 'Work item not found' });
3729
+ const feedbackNote = '# Human Feedback on ' + id + '\n\n' +
3730
+ '**Rating:** ' + (rating === 'up' ? '👍 Good' : '👎 Needs improvement') + '\n' +
3731
+ '**Item:** ' + found.title + '\n' +
3732
+ '**Agent:** ' + found.agent + '\n' +
3733
+ (comment ? '**Feedback:** ' + comment + '\n' : '');
3734
+ const inboxPath = path.join(MINIONS_DIR, 'notes', 'inbox', found.agent + '-feedback-' + new Date().toISOString().slice(0, 10) + '-' + shared.uid().slice(0, 4) + '.md');
3735
+ safeWrite(inboxPath, feedbackNote);
3736
+ invalidateStatusCache();
3737
+ return jsonReply(res, 200, { ok: true });
3749
3738
  }},
3750
3739
 
3751
3740
  // Pinned notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.531",
3
+ "version": "0.1.532",
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"