@yemi33/minions 0.1.2150 → 0.1.2152

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.
@@ -3,7 +3,7 @@
3
3
  [data-font-size] on <html>. 'small' is the historic default (1.0) so
4
4
  existing users see no change. Body-level CSS `zoom` is the cheapest
5
5
  way to scale every px/em/rem rule across the SPA (typography tokens
6
- below, slim.html, and the many inline `font-size:11px` styles) without
6
+ below, slim.html, and the many inline font-size Npx styles) without
7
7
  rewriting every rule. Scales modals, drawers, and fixed-position
8
8
  elements because they all live inside <body>. */
9
9
  --minions-font-scale: 1;
@@ -15,11 +15,30 @@
15
15
  --space-1: 2px; --space-2: 4px; --space-3: 6px; --space-4: 8px;
16
16
  --space-5: 10px; --space-6: 12px; --space-7: 16px; --space-8: 20px; --space-9: 24px;
17
17
 
18
- /* Typography scale */
18
+ /* Typography — size primitives (raw px). Source of truth for the
19
+ ~700 callsites migrated in PR #66; do NOT introduce new raw `Npx`
20
+ font-sizes outside this block. Tripwire:
21
+ test/unit/dashboard-font-size-tokens.test.js */
19
22
  --text-xs: 10px; --text-sm: 11px; --text-base: 12px;
20
23
  --text-md: 13px; --text-lg: 14px; --text-xl: 16px; --text-2xl: 18px;
21
24
  --text-stat: 22px; --text-stat-lg: 28px; --text-display: 32px;
22
25
 
26
+ /* Typography — role aliases (W-mq1c8og40003a7cf). Prefer these for
27
+ NEW code: the role names survive a size-scale redesign while the
28
+ primitives do not. Existing callsites that still reference the
29
+ size primitives directly are kept (they're correct by
30
+ construction — every role alias is just an indirection). See
31
+ dashboard/docs/typography.md for the role → primitive map and
32
+ intended use. */
33
+ --text-role-display: var(--text-display); /* 32px — hero / page title */
34
+ --text-heading: var(--text-2xl); /* 18px — section + modal headers */
35
+ --text-subheading: var(--text-xl); /* 16px — secondary headers, card titles */
36
+ --text-body: var(--text-lg); /* 14px — default body text */
37
+ --text-meta: var(--text-md); /* 13px — captions, timestamps, secondary metadata */
38
+ --text-caption: var(--text-base); /* 12px — small labels, table cells */
39
+ --text-micro: var(--text-sm); /* 11px — chip / tag pill labels */
40
+ --text-code: 0.9em; /* inline code; relative so it scales with the surrounding text */
41
+
23
42
  /* Border radius */
24
43
  --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-xl: 10px; --radius-full: 50%;
25
44
 
@@ -43,6 +62,21 @@
43
62
  html, body { height: 100%; margin: 0; overflow: hidden; }
44
63
  body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: var(--text-xl); display: flex; flex-direction: column; zoom: var(--minions-font-scale, 1); }
45
64
 
65
+ /* Typography utility classes (W-mq1c8og40003a7cf).
66
+ One class per role token. Apply directly on any element
67
+ (`<span class="text-meta">`) so JS/HTML callsites don't need to
68
+ hand-roll inline `style="font-size:var(--text-*)"`. The classes
69
+ ONLY set font-size — weight, color, line-height stay with the
70
+ caller. Mirrors the `.btn-add` consolidation in spirit. */
71
+ .text-display { font-size: var(--text-role-display); }
72
+ .text-heading { font-size: var(--text-heading); }
73
+ .text-subheading { font-size: var(--text-subheading); }
74
+ .text-body { font-size: var(--text-body); }
75
+ .text-meta { font-size: var(--text-meta); }
76
+ .text-caption { font-size: var(--text-caption); }
77
+ .text-micro { font-size: var(--text-micro); }
78
+ .text-code { font-size: var(--text-code); }
79
+
46
80
  header {
47
81
  background: var(--surface); border-bottom: 1px solid var(--border);
48
82
  padding: var(--space-6) 14px; display: flex; align-items: center; justify-content: space-between;
@@ -240,7 +274,7 @@
240
274
  .prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
241
275
  .prd-item-row.st-updated { border-left-color: var(--purple); }
242
276
  .prd-item-row.st-paused { border-left-color: var(--muted); opacity: 0.5; }
243
- .prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: 0.9em; }
277
+ .prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: var(--text-code); }
244
278
  .prd-item-name { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
245
279
  .prd-item-priority { font-size: var(--text-sm); padding: var(--space-1) var(--space-3); border-radius: var(--radius-lg); }
246
280
  .prd-item-priority.high { background: rgba(248,81,73,0.15); color: var(--red); }
package/engine/cli.js CHANGED
@@ -1304,15 +1304,17 @@ const commands = {
1304
1304
  console.log(`Engine: ${control.state} (PID ${control.pid || 'N/A'})`);
1305
1305
  }
1306
1306
 
1307
- // Dashboard check
1307
+ // Dashboard check. Honors MINIONS_PORT so `minions --dev status` probes the
1308
+ // dev dashboard (7332 by default) rather than the binary install's 7331.
1308
1309
  const http = require('http');
1310
+ const dashPort = Number(process.env.MINIONS_PORT) || 7331;
1309
1311
  const dashCheck = new Promise(resolve => {
1310
- const req = http.get('http://localhost:7331/api/health', { timeout: 2000 }, () => resolve(true));
1312
+ const req = http.get(`http://localhost:${dashPort}/api/health`, { timeout: 2000 }, () => resolve(true));
1311
1313
  req.on('error', () => resolve(false));
1312
1314
  req.on('timeout', () => { req.destroy(); resolve(false); });
1313
1315
  });
1314
1316
  dashCheck.then(dashUp => {
1315
- if (dashUp) console.log('Dashboard: running (http://localhost:7331)');
1317
+ if (dashUp) console.log(`Dashboard: running (http://localhost:${dashPort})`);
1316
1318
  else console.log('Dashboard: not running — start with: minions dash');
1317
1319
  }).catch(() => {});
1318
1320
 
@@ -18,6 +18,18 @@ const { getConfig, INBOX_DIR } = queries;
18
18
 
19
19
  const MINIONS_DIR = shared.MINIONS_DIR;
20
20
 
21
+ // Dispatch types that do not push code to a PR branch. These remain dispatchable
22
+ // against `_contextOnly: true` PRs because casting a review vote, posting a
23
+ // comment, asking a question, or running a read-only explore never mutates the
24
+ // PR's source branch. Auto-discovery (engine.js#discoverFromPrs) is still
25
+ // gated on `_contextOnly` separately — only explicit WIs (dashboard, watches,
26
+ // CC) can target a context-only PR, and only via these types.
27
+ const NON_MUTATING_DISPATCH_TYPES = new Set([
28
+ WORK_TYPE.REVIEW,
29
+ WORK_TYPE.ASK,
30
+ WORK_TYPE.EXPLORE,
31
+ ]);
32
+
21
33
  // Lazy require to break circular dependency with engine.js
22
34
  let _lifecycle = null;
23
35
  function lifecycle() { if (!_lifecycle) _lifecycle = require('./lifecycle'); return _lifecycle; }
@@ -358,7 +370,19 @@ function getStalePrDispatchReason(entry, config) {
358
370
  const prLabel = entry.meta.pr?.id || entry.meta.pr?.url || entry.id;
359
371
  if (!tracked) return `PR ${prLabel} is no longer tracked`;
360
372
  if (tracked.status !== PR_STATUS.ACTIVE) return `PR ${tracked.id || prLabel} is ${tracked.status || 'missing status'}`;
361
- if (tracked._contextOnly && entry.meta?.source !== 'work-item') return `PR ${tracked.id || prLabel} is context-only`;
373
+ // Combined gate (W-mq5v612m001g1545 + #3126):
374
+ // 1. `entry.meta?.source !== 'work-item'` — blocks any non-work-item dispatch
375
+ // (e.g. legacy `pr`/`pr-human-feedback` discovery paths) from running
376
+ // against a context-only PR. Auto-discovery in engine.js#discoverFromPrs
377
+ // already skips _contextOnly PRs at the source, so this is defense-in-depth.
378
+ // 2. `!NON_MUTATING_DISPATCH_TYPES.has(entry.type)` — even an explicit
379
+ // work-item dispatch is dropped if it's a mutating type (fix, implement,
380
+ // test, verify, decompose, docs). Only review/ask/explore — which never
381
+ // push to the PR's source branch — are allowed through.
382
+ if (tracked._contextOnly
383
+ && (entry.meta?.source !== 'work-item' || !NON_MUTATING_DISPATCH_TYPES.has(entry.type))) {
384
+ return `PR ${tracked.id || prLabel} is context-only`;
385
+ }
362
386
 
363
387
  const queuedBranch = entry.meta.branch || entry.meta.pr?.branch || '';
364
388
  const trackedBranch = tracked.branch || '';
@@ -1048,4 +1072,5 @@ module.exports = {
1048
1072
  findActivePrOrBranchLock,
1049
1073
  normalizeRetryableDecision,
1050
1074
  isCompletedWorkItemForFailure,
1075
+ NON_MUTATING_DISPATCH_TYPES,
1051
1076
  };
@@ -546,6 +546,64 @@ function resolveWorkItemPath(meta) {
546
546
  return null;
547
547
  }
548
548
 
549
+ function getWorkItemPaths(config) {
550
+ const paths = [path.join(MINIONS_DIR, 'work-items.json')];
551
+ for (const p of shared.getProjects(config || {})) {
552
+ if (p && p.name) paths.push(shared.projectWorkItemsPath(p));
553
+ }
554
+ return paths;
555
+ }
556
+
557
+ function findWorkItemRecord(itemId, config) {
558
+ if (!itemId) return null;
559
+ for (const wiPath of getWorkItemPaths(config)) {
560
+ try {
561
+ const items = readOptionalJsonStrict(wiPath, 'work-items', Array.isArray) || [];
562
+ const item = items.find(i => i && i.id === itemId);
563
+ if (item) return { item, path: wiPath };
564
+ } catch { /* best-effort lineage lookup */ }
565
+ }
566
+ return null;
567
+ }
568
+
569
+ function resolveWorkItemScheduleContext(item, config) {
570
+ if (!item || typeof item !== 'object') return {};
571
+ const seen = new Set();
572
+ let current = item;
573
+ let scheduleId = current._scheduleId || current._sourceScheduleId || '';
574
+ let scheduleWorkItemId = current._scheduleId ? current.id : (current._scheduleWorkItemId || '');
575
+ let projectName = current.project || '';
576
+ let parentItemId = current.parent_id || current.parentId || '';
577
+
578
+ while ((!scheduleId || !projectName) && parentItemId && !seen.has(parentItemId)) {
579
+ seen.add(parentItemId);
580
+ const found = findWorkItemRecord(parentItemId, config);
581
+ if (!found?.item) break;
582
+ current = found.item;
583
+ if (!scheduleId && (current._scheduleId || current._sourceScheduleId)) {
584
+ scheduleId = current._scheduleId || current._sourceScheduleId;
585
+ scheduleWorkItemId = current._scheduleId ? current.id : (current._scheduleWorkItemId || current.id);
586
+ }
587
+ if (!projectName && current.project) projectName = current.project;
588
+ parentItemId = current.parent_id || current.parentId || '';
589
+ }
590
+
591
+ return {
592
+ scheduleId: scheduleId || '',
593
+ scheduleWorkItemId: scheduleWorkItemId || '',
594
+ projectName: projectName || '',
595
+ };
596
+ }
597
+
598
+ function applyScheduleContextToPrEntry(entry, item, config) {
599
+ const ctx = resolveWorkItemScheduleContext(item, config);
600
+ if (ctx.scheduleId) {
601
+ entry._sourceScheduleId = ctx.scheduleId;
602
+ if (ctx.scheduleWorkItemId) entry._scheduleWorkItemId = ctx.scheduleWorkItemId;
603
+ }
604
+ return entry;
605
+ }
606
+
549
607
  /** Check if a work item is in a terminal completed state. */
550
608
  function isItemCompleted(item) {
551
609
  if (!item || typeof item !== 'object') return false;
@@ -920,6 +978,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
920
978
  sourcePlan: meta?.item?.sourcePlan || '',
921
979
  itemType: meta?.item?.itemType || ''
922
980
  };
981
+ applyScheduleContextToPrEntry(entry, meta?.item, config);
923
982
  // Issue #1772: one-off dispatches (e.g. human-initiated "review this PR" via CC)
924
983
  // must not enroll the discovered PR into the auto eval loop. Tag _contextOnly so
925
984
  // discoverFromPrs skips it for review/fix dispatch (still polled for status/comments).
@@ -1318,6 +1377,11 @@ function resolvePrFallbackProject(meta, config) {
1318
1377
  const match = shared.resolveProjectSource(meta.item.project, projects, { allowCentral: false }).project;
1319
1378
  if (match) return match;
1320
1379
  }
1380
+ const scheduleContext = resolveWorkItemScheduleContext(meta?.item, config);
1381
+ if (scheduleContext.projectName) {
1382
+ const match = shared.resolveProjectSource(scheduleContext.projectName, projects, { allowCentral: false }).project;
1383
+ if (match) return match;
1384
+ }
1321
1385
  return projects.length === 1 ? projects[0] : null;
1322
1386
  }
1323
1387
 
@@ -1488,6 +1552,7 @@ function _attachFoundPrToWi(found, meta, agentId, resultSummary, config) {
1488
1552
  sourcePlan: meta.item?.sourcePlan || '',
1489
1553
  itemType: meta.item?.itemType || '',
1490
1554
  };
1555
+ applyScheduleContextToPrEntry(entry, meta?.item, config);
1491
1556
  shared.upsertPullRequestRecord(shared.projectPrPath(found.project), entry, {
1492
1557
  project: found.project,
1493
1558
  itemId: meta.item.id,
@@ -4184,6 +4249,9 @@ function processCompletionFollowups(completion, agentId, dispatchItem, config) {
4184
4249
  }
4185
4250
  if (raw.length === 0) return [];
4186
4251
  const wiId = dispatchItem?.meta?.item?.id || 'N/A';
4252
+ const parentItem = dispatchItem?.meta?.item || null;
4253
+ const parentProject = dispatchItem?.meta?.project?.name || parentItem?.project || '';
4254
+ const parentSchedule = resolveWorkItemScheduleContext(parentItem, config);
4187
4255
  let existingIds = null;
4188
4256
  function loadExistingIds() {
4189
4257
  if (existingIds !== null) return existingIds;
@@ -4217,6 +4285,27 @@ function processCompletionFollowups(completion, agentId, dispatchItem, config) {
4217
4285
  if (allIds.size > 0 && !allIds.has(followupWiId)) {
4218
4286
  log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): claimed followup ${followupWiId} not found in any work-items.json — dispatch may have been deduped, reverted, or never landed`);
4219
4287
  }
4288
+ if (parentItem && (parentSchedule.scheduleId || parentProject)) {
4289
+ let stamped = false;
4290
+ for (const wiPath of getWorkItemPaths(config)) {
4291
+ mutateWorkItems(wiPath, items => {
4292
+ const target = Array.isArray(items) ? items.find(i => i && i.id === followupWiId) : null;
4293
+ if (!target) return items;
4294
+ if (!target.parent_id && target.id !== wiId) target.parent_id = wiId;
4295
+ if (!target.project && parentProject) target.project = parentProject;
4296
+ if (parentSchedule.scheduleId) {
4297
+ if (!target._sourceScheduleId) target._sourceScheduleId = parentSchedule.scheduleId;
4298
+ if (!target._scheduleWorkItemId) target._scheduleWorkItemId = parentSchedule.scheduleWorkItemId || wiId;
4299
+ }
4300
+ stamped = true;
4301
+ return items;
4302
+ });
4303
+ if (stamped) break;
4304
+ }
4305
+ if (stamped && parentSchedule.scheduleId) {
4306
+ log('info', `Followup audit (${agentId || 'unknown'} wi=${wiId}): linked ${followupWiId} to schedule ${parentSchedule.scheduleId}`);
4307
+ }
4308
+ }
4220
4309
  accepted.push({
4221
4310
  wi_id: followupWiId,
4222
4311
  title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2150",
3
+ "version": "0.1.2152",
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"
@@ -9,6 +9,11 @@
9
9
  "package:check": "node tools/check-package-boundary.js public",
10
10
  "package:prepare:public": "node tools/prepare-package.js public",
11
11
  "package:prepare:internal": "node tools/prepare-package.js internal",
12
+ "dev": "node bin/minions.js --dev restart",
13
+ "dev:start": "node bin/minions.js --dev start",
14
+ "dev:stop": "node bin/minions.js --dev stop",
15
+ "dev:dash": "node bin/minions.js --dev dash",
16
+ "dev:status": "node bin/minions.js --dev status",
12
17
  "test": "node test/run-parallel.js",
13
18
  "test:sequential": "node test/unit.test.js",
14
19
  "test:unit": "node test/run-parallel.js",