@yemi33/minions 0.1.360 → 0.1.362

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,12 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.360 (2026-04-06)
3
+ ## 0.1.362 (2026-04-06)
4
4
 
5
5
  ### Features
6
+ - Null-safe safeJson wrappers + dashboard crash guards (#203)
6
7
  - pipeline plan stage uses LLM to generate structured plan from meeting
7
8
  - show 'Converting to PRD' status instead of 'In Progress' during plan conversion
8
9
 
9
10
  ### Fixes
11
+ - archived PRD picker uses file key instead of positional index
10
12
  - check pull-requests.json for existing PRs before no-PR retry
11
13
  - smart no-PR handling — distinguish MCP stall from intentional no-PR
12
14
 
@@ -148,8 +148,11 @@ function renderPrdProgress(prog) {
148
148
 
149
149
  // Linked work item info
150
150
  const wi = wiById[i.id];
151
+ const wiAgent = wi?.dispatched_to ? (agentData.find(a => a.id === wi.dispatched_to) || {}) : null;
151
152
  const wiLabel = wi ? '<span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:3px;border:1px solid var(--border)" title="Work item: ' + escHtml(wi.id) + '">' +
152
- escHtml(wi.id) + (wi.dispatched_to ? ' → ' + escHtml(wi.dispatched_to) : '') + '</span>' : '';
153
+ escHtml(wi.id) + '</span>' : '';
154
+ const agentLabel = wiAgent ? '<span style="font-size:9px;color:var(--muted)" title="' + escHtml(wiAgent.name || wi.dispatched_to) + '">' +
155
+ (wiAgent.emoji || '') + ' ' + escHtml(wiAgent.name || wi.dispatched_to) + '</span>' : '';
153
156
 
154
157
  // Requeue button for failed items
155
158
  const canRequeue = wi && (wi.status === 'failed' || i.status === 'failed');
@@ -170,6 +173,7 @@ function renderPrdProgress(prog) {
170
173
  '<span class="prd-item-id">' + escHtml(i.id) + '</span>' +
171
174
  '<span class="prd-item-name" title="' + escHtml(i.name) + '">' + escHtml(i.name) + '</span>' +
172
175
  wiLabel +
176
+ agentLabel +
173
177
  requeueBtn +
174
178
  (projBadges ? '<span>' + projBadges + '</span>' : '') +
175
179
  (prLinks ? '<span>' + prLinks + '</span>' : '') +
@@ -320,7 +324,9 @@ function renderPrdProgress(prog) {
320
324
  const borderColor = statusColor(i.status);
321
325
  const src = escHtml(i.source || '');
322
326
  const iid = escHtml(i.id || '');
323
- const agent = wi[i.id]?.dispatched_to || '';
327
+ const agentId = wi[i.id]?.dispatched_to || '';
328
+ const agentInfo = agentId ? (agentData.find(a => a.id === agentId) || {}) : null;
329
+ const agentDisplay = agentInfo ? (agentInfo.emoji || '') + ' ' + escHtml(agentInfo.name || agentId) : (agentId ? escHtml(agentId) : '');
324
330
  const deps = (i.depends_on || []).join(', ');
325
331
  const wipAnim = i.status === 'dispatched' ? 'animation:prdWipPulse 2s infinite;' : '';
326
332
  html += '<div onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')" ' +
@@ -333,7 +339,7 @@ function renderPrdProgress(prog) {
333
339
  '<div style="color:var(--text);font-size:11px;line-height:1.3;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical">' + escHtml(i.name) + '</div>' +
334
340
  '<div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center">' +
335
341
  '<span class="prd-item-priority ' + (i.priority || '') + '" style="font-size:8px;padding:1px 4px">' + escHtml(i.priority || '') + '</span>' +
336
- (agent ? '<span style="font-size:8px;color:var(--muted)">' + escHtml(agent) + '</span>' : '') +
342
+ (agentDisplay ? '<span style="font-size:8px;color:var(--muted)">' + agentDisplay + '</span>' : '') +
337
343
  (function() {
338
344
  const w = wi[i.id];
339
345
  if (!w) return '';
@@ -484,7 +490,7 @@ function openArchivedPrdModal() {
484
490
  const done = g.items.filter(it => it.status === 'done').length;
485
491
  const failed = g.items.filter(it => it.status === 'failed').length;
486
492
  const completed = g.completedAt ? new Date(g.completedAt).toLocaleDateString() : '';
487
- return '<div class="plan-card" style="cursor:pointer;margin-bottom:8px" onclick="showArchivedPrdDetail(' + i + ')">' +
493
+ return '<div class="plan-card" style="cursor:pointer;margin-bottom:8px" onclick="showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">' +
488
494
  '<div class="plan-card-title" style="font-size:13px">' + escHtml(g.summary || g.file) + '</div>' +
489
495
  '<div class="plan-card-meta">' +
490
496
  (g.project ? '<span>' + escHtml(g.project) + '</span>' : '') +
@@ -504,16 +510,17 @@ function openArchivedPrdModal() {
504
510
  document.getElementById('modal').classList.add('open');
505
511
  }
506
512
 
507
- function showArchivedPrdDetail(idx) {
513
+ function showArchivedPrdDetail(idxOrFile) {
508
514
  const groups = window._archivedPrdGroups || [];
515
+ const idx = typeof idxOrFile === 'string' ? groups.findIndex(g => g.file === idxOrFile) : idxOrFile;
509
516
  const g = groups[idx];
510
517
  if (!g) return;
511
518
 
512
519
  // View mode toggle for archived
513
520
  const isGraph = window._archivedPrdViewMode !== 'list';
514
521
  const toggleHtml = '<div style="display:flex;gap:4px;margin-bottom:8px">' +
515
- '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(' + idx + ')">Graph</button>' +
516
- '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(' + idx + ')">List</button>' +
522
+ '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">Graph</button>' +
523
+ '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">List</button>' +
517
524
  '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);margin-left:auto" onclick="triggerVerify(\'' + escHtml(g.file) + '\')">Trigger Verify</button>' +
518
525
  '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="planUnarchive(\'' + escHtml(g.file) + '\',this)">Unarchive</button>' +
519
526
  '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="openArchivedPrdModal()">Back</button>' +
@@ -25,7 +25,7 @@ async function openSettings() {
25
25
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Engine</h3>' +
26
26
  '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
27
27
  settingsField('Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', 'How often the engine runs discovery + dispatch') +
28
- settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 3, '', 'Max agents working simultaneously') +
28
+ settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 5, '', 'Max agents working simultaneously') +
29
29
  settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
30
30
  settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
31
31
  settingsField('Max Turns', 'set-maxTurns', e.maxTurns || 100, '', 'Claude CLI --max-turns per agent') +
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, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects, DONE_STATUSES } = shared;
17
+ const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, getProjects: _getProjects, DONE_STATUSES } = shared;
18
18
  const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
19
19
  getSkills, getInbox, getNotesWithMeta, getPullRequests,
20
20
  getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
@@ -1206,7 +1206,7 @@ const server = http.createServer(async (req, res) => {
1206
1206
  const planPath = resolvePlanPath(body.source);
1207
1207
  if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
1208
1208
  // Pre-check: verify item exists before taking the lock
1209
- const preCheck = safeJson(planPath);
1209
+ const preCheck = safeJsonObj(planPath);
1210
1210
  const preItem = (preCheck.missing_features || []).find(f => f.id === body.itemId);
1211
1211
  if (!preItem) return jsonReply(res, 404, { error: 'item not found in plan' });
1212
1212
 
@@ -1258,7 +1258,7 @@ const server = http.createServer(async (req, res) => {
1258
1258
  if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
1259
1259
  const planPath = resolvePlanPath(body.source);
1260
1260
  if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
1261
- const plan = safeJson(planPath);
1261
+ const plan = safeJsonObj(planPath);
1262
1262
  const idx = (plan.missing_features || []).findIndex(f => f.id === body.itemId);
1263
1263
  if (idx < 0) return jsonReply(res, 404, { error: 'item not found in plan' });
1264
1264
 
@@ -2048,7 +2048,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2048
2048
  if (!body.source) return jsonReply(res, 400, { error: 'source required' });
2049
2049
  const planPath = resolvePlanPath(body.source);
2050
2050
  if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
2051
- const plan = safeJson(planPath);
2051
+ const plan = safeJsonObj(planPath);
2052
2052
  const planItems = plan.missing_features || [];
2053
2053
 
2054
2054
  let reset = 0, kept = 0, newCount = 0;
@@ -2127,7 +2127,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2127
2127
  }
2128
2128
  for (const wiPath of wiPaths) {
2129
2129
  try {
2130
- const items = safeJson(wiPath);
2130
+ const items = safeJsonArr(wiPath);
2131
2131
  const filtered = items.filter(w => w.sourcePlan !== body.file);
2132
2132
  if (filtered.length < items.length) {
2133
2133
  cleaned += items.length - filtered.length;
@@ -2834,7 +2834,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2834
2834
  if (!fs.existsSync(target)) return jsonReply(res, 400, { error: 'Directory not found: ' + target });
2835
2835
 
2836
2836
  const configPath = path.join(MINIONS_DIR, 'config.json');
2837
- const config = safeJson(configPath);
2837
+ const config = safeJsonObj(configPath);
2838
2838
  if (!config.projects) config.projects = [];
2839
2839
 
2840
2840
  // Check if already linked
package/engine/shared.js CHANGED
@@ -73,6 +73,12 @@ function safeJson(p) {
73
73
  }
74
74
  }
75
75
 
76
+ /** Null-safe safeJson wrapper — returns {} when file is missing/corrupt. */
77
+ function safeJsonObj(p) { return safeJson(p) || {}; }
78
+
79
+ /** Null-safe safeJson wrapper — returns [] when file is missing/corrupt. */
80
+ function safeJsonArr(p) { return safeJson(p) || []; }
81
+
76
82
  /**
77
83
  * Monotonic counter for generating unique temp file names within this process.
78
84
  * Assumes single-thread execution (no worker_threads). If worker_threads are
@@ -422,7 +428,7 @@ const DEFAULT_AGENTS = {
422
428
 
423
429
  const DEFAULT_CLAUDE = {
424
430
  binary: 'claude',
425
- outputFormat: 'json',
431
+ outputFormat: 'stream-json',
426
432
  allowedTools: 'Edit,Write,Read,Bash,Glob,Grep,Agent,WebFetch,WebSearch',
427
433
  };
428
434
 
@@ -623,7 +629,7 @@ module.exports = {
623
629
  log,
624
630
  safeRead,
625
631
  safeReadDir,
626
- safeJson,
632
+ safeJson, safeJsonObj, safeJsonArr,
627
633
  safeWrite,
628
634
  safeUnlink,
629
635
  withFileLock,
package/engine.js CHANGED
@@ -1416,7 +1416,12 @@ function discoverFromWorkItems(config, project) {
1416
1416
  // This protects against persisted state drift from old runtime versions.
1417
1417
  try {
1418
1418
  mutateDispatch((dp) => {
1419
- dp.completed = Array.isArray(dp.completed) ? dp.completed.filter(d => d.meta?.dispatchKey !== key) : [];
1419
+ const prev = Array.isArray(dp.completed) ? dp.completed : [];
1420
+ const next = [];
1421
+ for (let i = 0; i < prev.length; i++) {
1422
+ if (prev[i].meta?.dispatchKey !== key) next.push(prev[i]);
1423
+ }
1424
+ dp.completed = next;
1420
1425
  return dp;
1421
1426
  });
1422
1427
  dispatchCooldowns.delete(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.360",
3
+ "version": "0.1.362",
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"