@yemi33/minions 0.1.374 → 0.1.376

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,8 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.374 (2026-04-06)
3
+ ## 0.1.376 (2026-04-06)
4
+
5
+ ### Features
6
+ - dashboard write locking for work-items.json (#227)
4
7
 
5
8
  ### Fixes
9
+ - show red notification dot on Notes & KB when note is created
6
10
  - medium bugs — KB declaration, meeting phantom archive, lifecycle race (#216)
7
11
 
8
12
  ## 0.1.373 (2026-04-06)
@@ -377,6 +377,8 @@ async function ccExecuteAction(action) {
377
377
  await _ccFetch('/api/notes', { title: action.title, what: action.content || action.description, author: 'command-center' });
378
378
  status.innerHTML = '&#10003; Note saved: <strong>' + escHtml(action.title) + '</strong>';
379
379
  status.style.color = 'var(--green)';
380
+ var notePageLink = document.querySelector('.sidebar-link[data-page="inbox"]');
381
+ if (notePageLink && !notePageLink.querySelector('.notif-badge')) { var noteCurPage = document.querySelector('.sidebar-link.active')?.getAttribute('data-page'); if (noteCurPage !== 'inbox') showNotifBadge(notePageLink); }
380
382
  break;
381
383
  }
382
384
  case 'pin': {
@@ -201,7 +201,14 @@ async function submitQuickNote() {
201
201
  method: 'POST', headers: { 'Content-Type': 'application/json' },
202
202
  body: JSON.stringify({ title: title || 'Quick note', what: content || title })
203
203
  });
204
- if (res.ok) { refresh(); }
204
+ if (res.ok) {
205
+ refresh();
206
+ const currentPage = document.querySelector('.sidebar-link.active')?.getAttribute('data-page');
207
+ if (currentPage !== 'inbox') {
208
+ const link = document.querySelector('.sidebar-link[data-page="inbox"]');
209
+ if (link && !link.querySelector('.notif-badge')) showNotifBadge(link);
210
+ }
211
+ }
205
212
  else { const d = await res.json().catch(() => ({})); alert('Note failed: ' + (d.error || 'unknown')); openQuickNoteModal(); }
206
213
  } catch (e) { alert('Error saving note: ' + e.message); openQuickNoteModal(); }
207
214
  }
package/dashboard.js CHANGED
@@ -14,7 +14,7 @@ const shared = require('./engine/shared');
14
14
  const queries = require('./engine/queries');
15
15
  const os = require('os');
16
16
 
17
- const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects, DONE_STATUSES } = shared;
17
+ const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects, DONE_STATUSES, WI_STATUS } = shared;
18
18
  const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
19
19
  getSkills, getInbox, getNotesWithMeta, getPullRequests,
20
20
  getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
@@ -972,7 +972,7 @@ const server = http.createServer(async (req, res) => {
972
972
  }, { defaultValue: [] });
973
973
  if (!found) return jsonReply(res, 404, { error: 'item not found' });
974
974
 
975
- // Clean dispatch entries + kill running agent
975
+ // Clean dispatch entries + kill running agent (outside lock)
976
976
  const dispatchRemoved = cleanDispatchEntries(d =>
977
977
  d.meta?.item?.id === id ||
978
978
  d.meta?.dispatchKey?.endsWith(id)
@@ -1011,21 +1011,24 @@ const server = http.createServer(async (req, res) => {
1011
1011
  }
1012
1012
  if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
1013
1013
 
1014
- const items = JSON.parse(safeRead(wiPath) || '[]');
1015
- const idx = items.findIndex(i => i.id === id);
1016
- if (idx === -1) return jsonReply(res, 404, { error: 'item not found' });
1017
-
1018
- const item = items.splice(idx, 1)[0];
1019
- item.archivedAt = new Date().toISOString();
1014
+ let archivedItem = null;
1015
+ mutateJsonFileLocked(wiPath, (items) => {
1016
+ if (!Array.isArray(items)) items = [];
1017
+ const idx = items.findIndex(i => i.id === id);
1018
+ if (idx === -1) return items;
1019
+ archivedItem = items.splice(idx, 1)[0];
1020
+ archivedItem.archivedAt = new Date().toISOString();
1021
+ return items;
1022
+ });
1023
+ if (!archivedItem) return jsonReply(res, 404, { error: 'item not found' });
1020
1024
 
1021
- // Append to archive file
1025
+ // Append to archive file (outside lock)
1022
1026
  const archivePath = wiPath.replace('.json', '-archive.json');
1023
- let archive = [];
1024
- const existing = safeRead(archivePath);
1025
- if (existing) { try { archive = JSON.parse(existing); } catch {} }
1026
- archive.push(item);
1027
- safeWrite(archivePath, archive);
1028
- safeWrite(wiPath, items);
1027
+ mutateJsonFileLocked(archivePath, (archive) => {
1028
+ if (!Array.isArray(archive)) archive = [];
1029
+ archive.push(archivedItem);
1030
+ return archive;
1031
+ }, { defaultValue: [] });
1029
1032
 
1030
1033
  // Clean dispatch entries for archived item
1031
1034
  const sourcePrefix = (!source || source === 'central') ? 'central-work-' : `work-${source}-`;
@@ -1069,14 +1072,11 @@ const server = http.createServer(async (req, res) => {
1069
1072
  // Write to central queue — agent decides which project
1070
1073
  wiPath = path.join(MINIONS_DIR, 'work-items.json');
1071
1074
  }
1072
- let items = [];
1073
- const existing = safeRead(wiPath);
1074
- if (existing) { try { items = JSON.parse(existing); } catch {} }
1075
1075
  const id = 'W-' + shared.uid();
1076
1076
  const item = {
1077
1077
  id, title: body.title, type: body.type || 'implement',
1078
1078
  priority: body.priority || 'medium', description: body.description || '',
1079
- status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard',
1079
+ status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
1080
1080
  };
1081
1081
  if (body.scope) item.scope = body.scope;
1082
1082
  if (body.agent) item.agent = body.agent;
@@ -1084,8 +1084,11 @@ const server = http.createServer(async (req, res) => {
1084
1084
  if (body.references) item.references = body.references;
1085
1085
  if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
1086
1086
  if (body.skipPr) item.skipPr = true;
1087
- items.push(item);
1088
- safeWrite(wiPath, items);
1087
+ mutateJsonFileLocked(wiPath, (items) => {
1088
+ if (!Array.isArray(items)) items = [];
1089
+ items.push(item);
1090
+ return items;
1091
+ });
1089
1092
  return jsonReply(res, 200, { ok: true, id });
1090
1093
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1091
1094
  }
@@ -1107,29 +1110,32 @@ const server = http.createServer(async (req, res) => {
1107
1110
  }
1108
1111
  if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
1109
1112
 
1110
- const items = JSON.parse(safeRead(wiPath) || '[]');
1111
- const item = items.find(i => i.id === id);
1112
- if (!item) return jsonReply(res, 404, { error: 'item not found' });
1113
-
1114
- if (item.status === 'dispatched') {
1115
- return jsonReply(res, 400, { error: 'Cannot edit dispatched items' });
1116
- }
1117
-
1118
- if (title !== undefined) item.title = title;
1119
- if (description !== undefined) item.description = description;
1120
- if (type !== undefined) item.type = type;
1121
- if (priority !== undefined) item.priority = priority;
1122
- if (agent !== undefined) {
1123
- item.agent = agent || null;
1124
- // Clear stale pending dispatch entries so the engine re-queues with the new agent
1125
- cleanDispatchEntries(d => d.meta?.item?.id === id);
1126
- }
1127
- if (body.references !== undefined) item.references = body.references;
1128
- if (body.acceptanceCriteria !== undefined) item.acceptanceCriteria = body.acceptanceCriteria;
1129
- item.updatedAt = new Date().toISOString();
1130
-
1131
- safeWrite(wiPath, items);
1132
- return jsonReply(res, 200, { ok: true, item });
1113
+ let result = null;
1114
+ let agentChanged = false;
1115
+ mutateJsonFileLocked(wiPath, (items) => {
1116
+ if (!Array.isArray(items)) items = [];
1117
+ const item = items.find(i => i.id === id);
1118
+ if (!item) { result = { code: 404, body: { error: 'item not found' } }; return items; }
1119
+ if (item.status === WI_STATUS.DISPATCHED) { result = { code: 400, body: { error: 'Cannot edit dispatched items' } }; return items; }
1120
+
1121
+ if (title !== undefined) item.title = title;
1122
+ if (description !== undefined) item.description = description;
1123
+ if (type !== undefined) item.type = type;
1124
+ if (priority !== undefined) item.priority = priority;
1125
+ if (agent !== undefined) {
1126
+ item.agent = agent || null;
1127
+ agentChanged = true;
1128
+ }
1129
+ if (body.references !== undefined) item.references = body.references;
1130
+ if (body.acceptanceCriteria !== undefined) item.acceptanceCriteria = body.acceptanceCriteria;
1131
+ item.updatedAt = new Date().toISOString();
1132
+ result = { code: 200, body: { ok: true, item } };
1133
+ return items;
1134
+ });
1135
+ if (!result) return jsonReply(res, 500, { error: 'unexpected state' });
1136
+ // Clear stale pending dispatch entries outside lock
1137
+ if (agentChanged) cleanDispatchEntries(d => d.meta?.item?.id === id);
1138
+ return jsonReply(res, result.code, result.body);
1133
1139
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1134
1140
  }
1135
1141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.374",
3
+ "version": "0.1.376",
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"