@yemi33/minions 0.1.2194 → 0.1.2196

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/engine/queries.js CHANGED
@@ -496,6 +496,72 @@ function getInboxFiles() {
496
496
  try { return fs.readdirSync(INBOX_DIR).filter(f => f.endsWith('.md')); } catch { return []; }
497
497
  }
498
498
 
499
+ // P-34fa5d79 — Note ↔ Work Item ↔ KB Entry linkage.
500
+ //
501
+ // Parse the YAML frontmatter from a note body and return the parsed fields.
502
+ // Same minimal contract as engine/kb-sweep.js#_parseFrontmatter — a single
503
+ // shared regex would have been nicer but importing kb-sweep from queries
504
+ // would form a circular dep with lifecycle (queries → kb-sweep → … →
505
+ // queries), so we keep a local copy of the four-line parser.
506
+ function _parseNoteFrontmatter(content) {
507
+ const m = String(content || '').match(/^---\n([\s\S]*?)\n---\n?/);
508
+ if (!m) return null;
509
+ const fm = {};
510
+ for (const line of m[1].split('\n')) {
511
+ const lm = line.match(/^([\w-]+):\s*(.*)$/);
512
+ if (lm) fm[lm[1]] = lm[2].trim();
513
+ }
514
+ return fm;
515
+ }
516
+
517
+ // Extract the WI id that a note's frontmatter cites, or null. Accepts the
518
+ // three field-name conventions used across the repo today:
519
+ // - `work_item:` (dallas fix-summary template, the common case)
520
+ // - `wi:` (terser variant, used in a handful of newer notes)
521
+ // - `sourceItem:` (engine-emitted notes such as pr-auto-link-unverified)
522
+ // `id:` itself is the note's own NOTE-… id and is intentionally NOT matched —
523
+ // it would never carry a WI value in any existing convention.
524
+ function _wiIdFromNoteFrontmatter(fm) {
525
+ if (!fm) return null;
526
+ return fm.work_item || fm.wi || fm.sourceItem || null;
527
+ }
528
+
529
+ // Build a wiId → [filename, ...] map by scanning the inbox + archive dirs.
530
+ // Archive entries are prefixed with `archive:` so the dashboard renderer can
531
+ // tell them apart (matches the convention already used by _artifacts.notes).
532
+ // Pure-function helper exposed for testing.
533
+ function _buildNotesByWiMap() {
534
+ const out = Object.create(null);
535
+ const addNote = (wiId, token) => {
536
+ if (!wiId || !token) return;
537
+ if (!out[wiId]) out[wiId] = [];
538
+ if (!out[wiId].includes(token)) out[wiId].push(token);
539
+ };
540
+ for (const f of safeReadDir(INBOX_DIR)) {
541
+ if (!f.endsWith('.md')) continue;
542
+ const fm = _parseNoteFrontmatter(safeRead(path.join(INBOX_DIR, f)));
543
+ const wiId = _wiIdFromNoteFrontmatter(fm);
544
+ if (wiId) addNote(wiId, f);
545
+ }
546
+ for (const f of safeReadDir(ARCHIVE_DIR)) {
547
+ if (!f.endsWith('.md')) continue;
548
+ const fm = _parseNoteFrontmatter(safeRead(path.join(ARCHIVE_DIR, f)));
549
+ const wiId = _wiIdFromNoteFrontmatter(fm);
550
+ if (wiId) addNote(wiId, 'archive:' + f);
551
+ }
552
+ return out;
553
+ }
554
+
555
+ // Public lookup — return the array of note tokens (filenames or
556
+ // `archive:<filename>`) whose frontmatter cites this WI id. Returns [] when
557
+ // nothing matches. Scans inbox + archive each call; for the bulk WI list
558
+ // payload getWorkItems() pre-builds the map once per cache cycle.
559
+ function notesForWorkItem(wiId) {
560
+ if (!wiId) return [];
561
+ const map = _buildNotesByWiMap();
562
+ return map[wiId] || [];
563
+ }
564
+
499
565
  function getInbox() {
500
566
  return safeReadDir(INBOX_DIR)
501
567
  .filter(f => f.endsWith('.md'))
@@ -1745,6 +1811,9 @@ function getWorkItems(config) {
1745
1811
  // Use snapshot — sync access; cold start before any async warm returns [].
1746
1812
  // Best-effort enrichment for work item _artifacts.notes, not correctness-critical.
1747
1813
  const _kbEntries = getKnowledgeBaseEntriesSnapshot();
1814
+ // P-34fa5d79 — pre-build the wiId → notes map so each item's _notes lookup
1815
+ // is O(1) instead of re-scanning inbox+archive per item.
1816
+ const _notesByWi = _buildNotesByWiMap();
1748
1817
  for (const item of allItems) {
1749
1818
  const arts = shared.isPlainObject(item._artifacts) ? { ...item._artifacts } : {};
1750
1819
  const agentId = item.dispatched_to || item.agent;
@@ -1779,6 +1848,13 @@ function getWorkItems(config) {
1779
1848
  else if (item.planFile) arts.plan = item.planFile;
1780
1849
  if (item._pr) arts.pr = item._pr;
1781
1850
  if (Object.keys(arts).length > 0) item._artifacts = arts;
1851
+ // P-34fa5d79 — Note ↔ WI ↔ KB linkage. Slim string[] field of note
1852
+ // filenames whose YAML frontmatter cites this WI. Archive notes are
1853
+ // prefixed with `archive:` so the dashboard chip renderer can pick the
1854
+ // right open path. Field is left undefined (not []) when empty so the
1855
+ // /api/work-items JSON stays compact for the common no-notes case.
1856
+ const mentions = _notesByWi[item.id];
1857
+ if (mentions && mentions.length > 0) item._notes = mentions;
1782
1858
  }
1783
1859
 
1784
1860
  const statusOrder = {
@@ -2897,6 +2973,11 @@ module.exports = {
2897
2973
 
2898
2974
  // Inbox
2899
2975
  getInboxFiles, getInbox,
2976
+ // P-34fa5d79 — Note ↔ Work Item ↔ KB Entry linkage.
2977
+ notesForWorkItem,
2978
+ _parseNoteFrontmatter, // exported for testing
2979
+ _wiIdFromNoteFrontmatter, // exported for testing
2980
+ _buildNotesByWiMap, // exported for testing
2900
2981
 
2901
2982
  // Agents
2902
2983
  getAgentStatus, getAgentCharter, getAgents, getAgentDetail,
@@ -328,6 +328,12 @@ function createScheduledWorkItem(sched) {
328
328
  ...(sched.agentLock === true || sched.hardAgent === true ? { agentLock: true } : {}),
329
329
  project: sched.project || null,
330
330
  _scheduleId: sched.id,
331
+ // P-c549d07e — back-link to the schedule that spawned this WI. The WI
332
+ // detail modal renders this as a clickable chip via renderArtifactLink
333
+ // so the operator can navigate back to the dispatcher. Kept alongside
334
+ // _scheduleId (used by queries / lifecycle / consolidation) so we don't
335
+ // disturb the existing readers — meta.spawnedBy is purely additive.
336
+ meta: { spawnedBy: 'schedule:' + sched.id },
331
337
  };
332
338
  // Walk every string-valued field on the work item so vars embedded in
333
339
  // nested fields (e.g. _harness.rubric added by callers, references[].url,
@@ -339,7 +345,19 @@ function createScheduledWorkItem(sched) {
339
345
 
340
346
  function writeScheduleRunEntry(runs, scheduleId, workItemId, extra) {
341
347
  const existing = typeof runs[scheduleId] === 'object' && runs[scheduleId] ? runs[scheduleId] : {};
342
- runs[scheduleId] = { ...existing, lastRun: ts(), lastWorkItemId: workItemId, ...(extra || {}) };
348
+ // P-c549d07e maintain a recentWorkItemIds ring (newest first, hard cap 5,
349
+ // deduplicated). Surfaces "Recent dispatches" chips in the schedule modal
350
+ // without forcing the dashboard to scan every WI for _scheduleId matches.
351
+ // Empty / falsy workItemId leaves the ring untouched (defensive — callers
352
+ // always pass a real id today, but the engine should never pollute the
353
+ // history with blanks if a future caller slips up).
354
+ let recent = Array.isArray(existing.recentWorkItemIds) ? existing.recentWorkItemIds.slice() : [];
355
+ if (workItemId) {
356
+ recent = recent.filter((id) => id !== workItemId);
357
+ recent.unshift(workItemId);
358
+ if (recent.length > 5) recent = recent.slice(0, 5);
359
+ }
360
+ runs[scheduleId] = { ...existing, lastRun: ts(), lastWorkItemId: workItemId, recentWorkItemIds: recent, ...(extra || {}) };
343
361
  return runs[scheduleId];
344
362
  }
345
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2194",
3
+ "version": "0.1.2196",
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"