@yemi33/minions 0.1.2150 → 0.1.2151

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/bin/minions.js CHANGED
@@ -64,6 +64,10 @@ const PKG_ROOT = path.resolve(__dirname, '..');
64
64
  const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
65
65
  const { openUrlInBrowser } = shared;
66
66
  const { waitForRestartHealth, formatRestartHealthError } = require(path.join(PKG_ROOT, 'engine', 'restart-health'));
67
+ // Dev-mode dashboard port. `--dev` mode (see argv parser below) retargets
68
+ // MINIONS_HOME at PKG_ROOT and routes the dashboard onto this port so a dev
69
+ // checkout coexists with a global npm install on the normal port (7331).
70
+ const DEFAULT_DEV_DASH_PORT = 7332;
67
71
 
68
72
  /**
69
73
  * Resolve the dashboard port for any CLI command. Precedence (W-mq5nwl9l):
@@ -71,7 +75,7 @@ const { waitForRestartHealth, formatRestartHealthError } = require(path.join(PKG
71
75
  * ACTUALLY bound to. Wins over the chain so status/dash/restart-health
72
76
  * probes the right port even after an EADDRINUSE auto-fallback.
73
77
  * 2. Explicit `--port N` CLI flag if present in `argv`.
74
- * 3. env MINIONS_DASHBOARD_PORT.
78
+ * 3. env MINIONS_DASHBOARD_PORT (dev mode pre-seeds this with devPort).
75
79
  * 4. config.json#engine.dashboardPort.
76
80
  * 5. shared.DEFAULT_DASHBOARD_PORT (7331).
77
81
  *
@@ -578,7 +582,50 @@ function resolveMinionsHome(forInit = false) {
578
582
  return shared.resolveMinionsHome(forInit);
579
583
  }
580
584
 
581
- const [cmd, ...rest] = process.argv.slice(2);
585
+ // `--dev` runs the in-place checkout AS the runtime root, on a separate
586
+ // dashboard port (default 7332) so it can coexist with a global npm install on
587
+ // 7331. Only valid when bin/minions.js sits inside a git checkout — the
588
+ // `.git` probe stops the global shim (whose PKG_ROOT lives under
589
+ // node_modules/) from accidentally activating dev mode.
590
+ //
591
+ // We strip --dev / --dev-port <n> from argv BEFORE the cmd/rest split so the
592
+ // flag can appear in any position (`minions --dev help`, `minions help --dev`,
593
+ // `minions --dev start --foo` all work). Otherwise `--dev` as the first arg
594
+ // would be misread as the subcommand.
595
+ const { cmd, rest, devMode, devPort } = (() => {
596
+ const raw = process.argv.slice(2);
597
+ const out = [];
598
+ let dev = process.env.MINIONS_DEV === '1';
599
+ let port = null;
600
+ for (let i = 0; i < raw.length; i++) {
601
+ const arg = raw[i];
602
+ if (arg === '--dev') { dev = true; continue; }
603
+ if (arg === '--dev-port') {
604
+ const next = raw[i + 1];
605
+ const n = Number(next);
606
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
607
+ console.error(`\n --dev-port requires an integer 1-65535, got: ${next || '(missing)'}\n`);
608
+ process.exit(2);
609
+ }
610
+ port = n; dev = true; i++; continue;
611
+ }
612
+ if (arg && arg.startsWith('--dev-port=')) {
613
+ const n = Number(arg.slice('--dev-port='.length));
614
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
615
+ console.error(`\n --dev-port=<n> requires an integer 1-65535, got: ${arg}\n`);
616
+ process.exit(2);
617
+ }
618
+ port = n; dev = true; continue;
619
+ }
620
+ out.push(arg);
621
+ }
622
+ if (dev && port == null) {
623
+ const envPort = Number(process.env.MINIONS_DEV_PORT);
624
+ if (Number.isInteger(envPort) && envPort > 0 && envPort <= 65535) port = envPort;
625
+ }
626
+ const [firstCmd, ...restArgs] = out;
627
+ return { cmd: firstCmd, rest: restArgs, devMode: dev, devPort: port || DEFAULT_DEV_DASH_PORT };
628
+ })();
582
629
  let force = rest.includes('--force');
583
630
  const skipScan = rest.includes('--skip-scan');
584
631
  const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
@@ -587,8 +634,46 @@ const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
587
634
  // when the heuristic mis-fires (e.g. you closed your dashboard tab seconds before
588
635
  // running restart and its final heartbeat was still inside the 45s window).
589
636
  const forceOpen = rest.includes('--open') || process.env.MINIONS_FORCE_OPEN === '1';
590
- const MINIONS_HOME = resolveMinionsHome(cmd === 'init');
637
+ // When --dev is set, PKG_ROOT MUST be a git checkout. Without the .git probe a
638
+ // user who typed `minions --dev` from any directory would silently retarget the
639
+ // globally-installed package as its own dev workspace.
640
+ if (devMode && !fs.existsSync(path.join(PKG_ROOT, '.git'))) {
641
+ console.error(`
642
+ --dev requires running from a git checkout of minions.
643
+ This bin/minions.js lives under: ${PKG_ROOT}
644
+ No .git directory was found there — refusing to retarget runtime files.
645
+
646
+ To use dev mode, clone the repo and run from inside it:
647
+ git clone https://github.com/opg-microsoft/minions.git
648
+ cd minions && node bin/minions.js --dev restart
649
+ `);
650
+ process.exit(2);
651
+ }
652
+
653
+ const MINIONS_HOME = devMode ? PKG_ROOT : resolveMinionsHome(cmd === 'init');
591
654
  process.env.MINIONS_HOME = MINIONS_HOME;
655
+ // When --dev is set, force the dashboard chain onto the dev port (7332 by
656
+ // default or --dev-port N) so a dev checkout coexists with a global install
657
+ // on 7331. We seed MINIONS_DASHBOARD_PORT so resolveDashboardPort()'s env
658
+ // step treats devPort as the fallback while still respecting an existing dev
659
+ // runtime-file at PKG_ROOT/engine/dashboard-port.json (runtime-file wins).
660
+ if (devMode) process.env.MINIONS_DASHBOARD_PORT = String(devPort);
661
+ // Effective dashboard port for this CLI invocation. Honors the W-mq5nwl9l
662
+ // chain (runtime-file → --port → env → config → DEFAULT_DASHBOARD_PORT/7331);
663
+ // --dev overrides via the env seed above. Consumed by the watchdog tick
664
+ // callsite below and propagated to children via MINIONS_PORT.
665
+ // Named DASHBOARD_PORT (not the deprecated DASH-underscore-PORT identifier) so
666
+ // the W-mq5nwl9l source-inspection guard in
667
+ // test/unit/dashboard-port-resolution.test.js keeps holding: that test pins
668
+ // that no static `\bDASH_PORT\b` survives, because anything other than the
669
+ // dynamic chain re-introduces the 7331 bug W-mq5nwl9l was opened to fix.
670
+ const DASHBOARD_PORT = resolveDashboardPort(rest).port;
671
+ // Propagate the port to every child (engine, dashboard, delegated subcommands).
672
+ // engine/cli.js status reads MINIONS_PORT; dashboard.js reads PORT. We export
673
+ // MINIONS_PORT for the engine + watch-actions / pipeline / managed-spawn
674
+ // loopback callers that already honor it, and pass PORT explicitly when
675
+ // spawning dashboard.js.
676
+ process.env.MINIONS_PORT = String(DASHBOARD_PORT);
592
677
  const POST_UPDATE_INIT_TIMEOUT_MS = 120000;
593
678
  const POST_UPDATE_RESTART_TIMEOUT_MS = 60000;
594
679
 
@@ -1051,8 +1136,16 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1051
1136
  minions watchdog uninstall Remove the scheduled task (idempotent)
1052
1137
  minions watchdog status Show registration + last-run details from the OS scheduler
1053
1138
  minions watchdog tick One-shot probe + recovery (used by the scheduler; safe to run by hand)
1054
-
1055
- Runtime root: ${MINIONS_HOME}
1139
+ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1140
+ Dev mode (this checkout, contributors only):
1141
+ minions --dev <cmd> Run against this checkout instead of ~/.minions/
1142
+ MINIONS_HOME=<checkout>, dashboard on :7332.
1143
+ Coexists with the global install on :7331.
1144
+ minions --dev --dev-port <n> <cmd> Override the dev dashboard port.
1145
+ npm run dev Shortcut for: node bin/minions.js --dev restart
1146
+ ` : ''}
1147
+ Runtime root: ${MINIONS_HOME}${devMode ? ' [DEV]' : ''}${devMode ? `
1148
+ Dashboard port: ${DASHBOARD_PORT} [DEV]` : ''}
1056
1149
  `);
1057
1150
  } else if (cmd === 'init') {
1058
1151
  init();
@@ -1365,7 +1458,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1365
1458
  watchdog.tick({
1366
1459
  minionsHome: MINIONS_HOME,
1367
1460
  minionsBin,
1368
- dashPort: resolveDashboardPort([]).port,
1461
+ dashPort: DASHBOARD_PORT,
1369
1462
  readEnginePid,
1370
1463
  isPortListening,
1371
1464
  isStopIntentSet: shared.isStopIntentSet || (() => false),
@@ -0,0 +1,128 @@
1
+ # Dashboard typography audit (W-mq1c8og40003a7cf)
2
+
3
+ This document is the pre-migration audit that justified the token set
4
+ now declared in `dashboard/styles.css` + `dashboard/slim/styles.css`.
5
+ See `dashboard/docs/typography.md` for the contract.
6
+
7
+ ## Findings
8
+
9
+ ### Baseline before this WI
10
+
11
+ | Surface | font-size declarations | Already on tokens? |
12
+ | ----------------------------- | ---------------------: | ----------------------------- |
13
+ | `dashboard/styles.css` (main) | 179 | ✅ all `var(--text-*)` except `.prd-item-id { font-size: 0.9em }` (slipped past PR #66 tripwire because the pattern was px-only) |
14
+ | `dashboard/layout.html` | 20 | ✅ all `var(--text-*)` |
15
+ | `dashboard/pages/*.html` | 32 | ✅ all `var(--text-*)` |
16
+ | `dashboard/js/*.js` | ~530 | ✅ all `var(--text-*)` except `utils.js:263` `font-size:0.9em` (same em-pattern blind spot as above) |
17
+ | `dashboard/slim/styles.css` | 82 | ❌ all literals (slim was explicitly out of scope for PR #66) |
18
+ | `dashboard/slim/body.html` | 2 | ❌ inline literals |
19
+ | **Total** | **~845** | **~84 raw / non-tokenized** |
20
+
21
+ The main dashboard was already ~99% tokenized by PR #66
22
+ (`refactor(dashboard): anchor typography scale at 12px, migrate raw px
23
+ to tokens`). Two `0.9em` declarations slipped past its `\d+\s*px`
24
+ tripwire because the regex didn't catch the em form.
25
+
26
+ The slim UX prototype (`dashboard/slim/*`) was deliberately out of scope
27
+ for PR #66 — slim is a feature-flagged playground anchored at 16px
28
+ body, and its CSS is served from disk on every request so the human can
29
+ hand-edit it (`dashboard/slim/styles.css` header comment).
30
+
31
+ ### Distinct font-size values seen across the audit
32
+
33
+ (Unique px values, sorted.)
34
+
35
+ | px | Count | Maps to | Where |
36
+ | -: | -----:| ------------------ | ---------------------------------------------------------- |
37
+ | 10 | many | `--text-xs` | main dashboard chips/badges |
38
+ | 11 | many | `--text-sm` | main dashboard status badges |
39
+ | 12 | ~30 | `--text-base` | main dashboard captions + most slim chips/labels |
40
+ | 13 | ~26 | `--text-md` | main dashboard default + slim default |
41
+ | 14 | ~10 | `--text-lg` | main dashboard body + slim body |
42
+ | 15 | 1 | `--text-lg` (round −1) | slim `.topbar-title` |
43
+ | 16 | ~7 | `--text-xl` | main dashboard body, slim chat input + msg |
44
+ | 17 | 1 | `--text-2xl` (round +1) | slim `.agent-detail-name` |
45
+ | 18 | many | `--text-2xl` | main dashboard h2 / modal headers |
46
+ | 20 | 1 | `--text-2xl` (round −2) | slim `.icon-btn` |
47
+ | 22 | many | `--text-stat` | main + slim stat readouts |
48
+ | 24 | 1 | `--text-stat` (round −2) | slim `.member-emoji` (decorative emoji glyph) |
49
+ | 28 | many | `--text-stat-lg` | main + slim large readouts |
50
+ | 32 | many | `--text-display` | main dashboard hero / H1 |
51
+ | 34 | 1 | `--text-display` (round −2) | slim `.agent-detail-emoji` (decorative emoji) |
52
+ | em | | | |
53
+ | 0.9em | 3 | `--text-code` | inline `<code>` in main + slim |
54
+
55
+ ### Rounding decisions (5 sites)
56
+
57
+ The task contract says: *"prefer rounding to the nearest token over
58
+ inventing a new token. If rounding would visibly change the UI in a way
59
+ you can't justify, flag it in the PR description."*
60
+
61
+ All five rounded sites are within ±2px of their assigned token:
62
+
63
+ | Selector | Was | Now (token) | Δ | Justification |
64
+ | ------------------------------------- | --: | ------------------ | ---: | -------------------------------------------------------------- |
65
+ | `slim .topbar-title` | 15 | 14 `--text-lg` | −1px | Topbar title — 1px reduction not perceptible at glance |
66
+ | `slim .agent-detail-name` | 17 | 18 `--text-2xl` | +1px | Detail-modal heading — 1px increase matches modal-h3 elsewhere |
67
+ | `slim .icon-btn` | 20 | 18 `--text-2xl` | −2px | Hamburger / close icon — emoji-like glyph |
68
+ | `slim .member-emoji` | 24 | 22 `--text-stat` | −2px | Decorative emoji avatar |
69
+ | `slim .agent-detail-emoji` | 34 | 32 `--text-display`| −2px | Decorative emoji in detail modal |
70
+
71
+ Each rounded site has a `/* Rounded from Npx → Mpx … */` inline comment.
72
+ All five sit inside the feature-flagged slim UX, so the blast radius is
73
+ minimal even if the human decides to revert. If any rounding visibly
74
+ breaks the slim layout, define a one-off custom property scoped to that
75
+ selector and reference it instead of expanding the global token set.
76
+
77
+ ### Tokens chosen — and rejected
78
+
79
+ **Chosen** (7 role tokens + 1 code token, all alias size primitives):
80
+
81
+ `--text-role-display`, `--text-heading`, `--text-subheading`,
82
+ `--text-body`, `--text-meta`, `--text-caption`, `--text-micro`,
83
+ `--text-code`.
84
+
85
+ **Rejected**:
86
+
87
+ - `--text-h1` / `--text-h2` / `--text-h3` — semantic HTML headings don't
88
+ map 1:1 to visual roles in this dashboard (`<h1>` in `header` is 18px;
89
+ PRD section h1s render at 32px). Role names beat heading names.
90
+ - `--text-label` — would overlap with `--text-caption`. Same role.
91
+ - `--text-large` / `--text-medium` / `--text-small` — pure-size names
92
+ duplicate the primitives without adding semantics.
93
+ - Slim-specific tokens (e.g. `--text-slim-body: 15px`) — would let slim
94
+ drift further from main. Better to round once and converge.
95
+ - A separate `--icon-*` family for the decorative emoji glyphs — only
96
+ 3 sites, all single-use, and they all happen to round cleanly to an
97
+ existing primitive. Not worth a parallel token family yet.
98
+
99
+ ### Out of scope (deliberately)
100
+
101
+ - font-weight, line-height, letter-spacing, color — explicitly out of
102
+ scope per the task contract; this WI is size-only.
103
+ - font-family — no change.
104
+ - SVG `font-size='90'` inside the favicon `data:image/svg+xml` URL —
105
+ data URLs can't resolve CSS variables. Allowlisted.
106
+ - The 4 `:root[data-font-size="…"] { --minions-font-scale: … }` rules
107
+ declare the dashboard zoom scale; they're attribute selectors, not
108
+ text-size declarations. Allowlisted.
109
+
110
+ ## Regression guard
111
+
112
+ Extended `test/unit/dashboard-font-size-tokens.test.js`:
113
+
114
+ - Already covered: main `dashboard/styles.css`, `dashboard/layout.html`,
115
+ `dashboard/pages/*.html`, `dashboard/js/*.js` for raw `Npx` font-sizes.
116
+ - New in this WI:
117
+ - Extended file set to include `dashboard/slim/styles.css` and
118
+ `dashboard/slim/body.html`.
119
+ - Added pattern for `font-size: Nem|Nrem|N%` (catches the `0.9em`
120
+ blind spot that hid the two pre-existing inline-code declarations).
121
+ - Allowlisted the `--text-code: 0.9em` token declaration itself.
122
+ - Asserted that slim's typography tokens match the main ones
123
+ (drift-detection between the two stylesheets).
124
+
125
+ The test runs in `npm test` and `npm run test:sequential`. ESLint
126
+ remains scoped to XSS-prevention (`eslint.config.js`); we don't add a
127
+ second lint plugin for CSS literals because the pure-Node tripwire
128
+ already covers every file type.
@@ -0,0 +1,114 @@
1
+ # Dashboard typography tokens
2
+
3
+ > Single source of truth for text sizing across the Minions dashboard.
4
+ > Centralized in W-mq1c8og40003a7cf (follow-up to PR #66 size primitives
5
+ > and PR #80 `.btn-add` consolidation).
6
+
7
+ ## Why this exists
8
+
9
+ The dashboard SPA renders text from ~30 modules (`dashboard/js/render-*.js`,
10
+ `dashboard/pages/*.html`, `dashboard/styles.css`, plus the slim UX prototype
11
+ in `dashboard/slim/`). Without a shared token system, every callsite picks
12
+ its own pixel size. Cards drift, captions disagree, headers don't line up
13
+ across pages, and changing the design language costs ~700 edits.
14
+
15
+ Two pieces solve this together:
16
+
17
+ 1. **Size primitives** (`--text-xs` ... `--text-display`) — raw px values.
18
+ The 10 declared sizes cover every visual role the dashboard currently
19
+ uses. Anchored at `--text-base = 12px`. PR #66 migrated ~664 callsites
20
+ from raw `Npx` to these tokens.
21
+ 2. **Role aliases** (`--text-heading`, `--text-body`, etc.) — semantic
22
+ names that point at size primitives. Added in W-mq1c8og40003a7cf so
23
+ new code talks in roles, not px. The size primitive stays the single
24
+ source of truth; role names are aliases.
25
+
26
+ ## Files this lives in
27
+
28
+ | File | What it owns |
29
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
30
+ | `dashboard/styles.css` | Canonical token declarations (`:root`), utility classes, and main-dashboard CSS rules. |
31
+ | `dashboard/slim/styles.css` | Duplicate token declarations + utility classes for the feature-flagged slim UX (self-contained stylesheet). |
32
+ | `dashboard/docs/typography.md` | This file — the contract. |
33
+ | `dashboard/docs/typography-audit.md` | The pre-migration audit + rounding decisions. |
34
+ | `test/unit/dashboard-font-size-tokens.test.js` | Tripwire that fails CI if raw `Npx` / `Nem` font-sizes leak back in. |
35
+
36
+ A tripwire asserts the slim token VALUES match the main values so the
37
+ two stay in lockstep.
38
+
39
+ ## Size primitives
40
+
41
+ | Token | Value | Common use |
42
+ | ------------------ | ----- | --------------------------------------------------------- |
43
+ | `--text-xs` | 10px | Smallest legitimate text; sidebar badges, count pills |
44
+ | `--text-sm` | 11px | Status badges, chip / tag labels |
45
+ | `--text-base` | 12px | Captions, secondary metadata, dense table cells |
46
+ | `--text-md` | 13px | Default for slim chat / detail tables / button labels |
47
+ | `--text-lg` | 14px | Body text, modal textarea, card titles |
48
+ | `--text-xl` | 16px | Main `<body>` font, secondary headers, chat input |
49
+ | `--text-2xl` | 18px | Section headers, page headers, modal h3 |
50
+ | `--text-stat` | 22px | Stat counter readouts, member emoji glyphs |
51
+ | `--text-stat-lg` | 28px | Larger stat readouts, completions-card type emoji |
52
+ | `--text-display` | 32px | Page hero / markdown H1, agent-detail emoji |
53
+ | `--text-code` | 0.9em | Inline `<code>` — relative so it scales with parent text |
54
+
55
+ > `--text-code` is intentionally relative (`em`, not `px`). Inline code
56
+ > should read 10% smaller than whatever surrounds it. This is the one
57
+ > token that breaks the "all px" rule and is explicitly allowlisted in
58
+ > the tripwire.
59
+
60
+ ## Role aliases (preferred for new code)
61
+
62
+ Use the role token when adding new UI. It survives a future size-scale
63
+ redesign without callsite edits.
64
+
65
+ | Role token | Aliases | Intended use |
66
+ | ---------------------- | ------------------- | ----------------------------------------- |
67
+ | `--text-role-display` | `--text-display` | Page hero, dashboard title |
68
+ | `--text-heading` | `--text-2xl` (18px) | Section + modal headers |
69
+ | `--text-subheading` | `--text-xl` (16px) | Secondary headers, card titles |
70
+ | `--text-body` | `--text-lg` (14px) | Default body text, paragraphs |
71
+ | `--text-meta` | `--text-md` (13px) | Captions, timestamps, secondary metadata |
72
+ | `--text-caption` | `--text-base` (12px)| Small labels, table cells |
73
+ | `--text-micro` | `--text-sm` (11px) | Chip / tag pill labels |
74
+ | `--text-code` | (`0.9em`) | Inline code |
75
+
76
+ ## Utility classes
77
+
78
+ Each role token has a matching utility class for callsites that can't
79
+ write a CSS rule (inline `class="text-meta"`, template strings, etc.).
80
+ Only `font-size` is set — weight, color, line-height stay with the caller.
81
+
82
+ ```html
83
+ <span class="text-meta">3 min ago</span>
84
+ <h3 class="text-heading">Active dispatches</h3>
85
+ <code class="text-code">npm run lint</code>
86
+ ```
87
+
88
+ Available classes: `.text-display`, `.text-heading`, `.text-subheading`,
89
+ `.text-body`, `.text-meta`, `.text-caption`, `.text-micro`, `.text-code`.
90
+
91
+ ## How to add a new text style
92
+
93
+ 1. Pick an existing role token. The 7 roles + 1 code token cover every
94
+ case the audit found.
95
+ 2. If your value doesn't fit any role, round to the nearest primitive
96
+ and document it inline. Inventing a new token requires changing
97
+ `dashboard/styles.css`, `dashboard/slim/styles.css`, this doc, AND
98
+ the tripwire.
99
+ 3. NEVER hand-roll `font-size: 14px` in CSS / inline style / JS template
100
+ string. The tripwire test `test/unit/dashboard-font-size-tokens.test.js`
101
+ fails CI.
102
+
103
+ ## Exceptions / allowlist
104
+
105
+ These declarations are intentional and exempt from the tripwire:
106
+
107
+ - `:root[data-font-size="…"]` rules in `dashboard/styles.css` — these
108
+ declare the user-level zoom scale, not text size.
109
+ - SVG `font-size='90'` inside the `data:image/svg+xml` favicon link in
110
+ `dashboard/layout.html` and `dashboard/slim/layout.html` — runtime
111
+ data URL, CSS variables don't resolve inside it.
112
+ - The `--text-*` token declarations themselves in `:root` blocks.
113
+
114
+ Everything else MUST go through a token.
@@ -82,7 +82,7 @@ async function openAgentDetail(id) {
82
82
  // guarantees no HTML interpretation even if the escape function were ever bypassed.
83
83
  const nameEl = document.getElementById('detail-agent-name');
84
84
  const emojiSpan = document.createElement('span');
85
- emojiSpan.style.fontSize = '22px';
85
+ emojiSpan.style.fontSize = 'var(--text-stat)';
86
86
  emojiSpan.textContent = agent.emoji || '';
87
87
  // Runtime tag \u2014 uses the inline-SVG logo from the same RUNTIME_TAGS map the
88
88
  // card uses, so the visual is consistent. The container's user-controlled
@@ -280,7 +280,7 @@ function _renderMdCore(s) {
280
280
  }).join('\n');
281
281
 
282
282
  html = html.replace(/`([^`\n]+)`/g, function(_, code) {
283
- codeSlots.push('<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:0.9em">' + code + '</code>');
283
+ codeSlots.push('<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:var(--text-code)">' + code + '</code>');
284
284
  return '\x00CB' + (codeSlots.length - 1) + '\x00';
285
285
  });
286
286
 
@@ -300,10 +300,10 @@
300
300
  <div class="modal-body">
301
301
  <p>Point to a local git repository. Minions will read its remote, name, and main branch automatically.</p>
302
302
  <div style="display:flex; gap:8px; align-items:stretch">
303
- <input id="slim-add-project-path" type="text" placeholder="C:\path\to\repo" style="flex:1; padding:8px 10px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); color:var(--text); font-size:13px; font-family:inherit">
303
+ <input id="slim-add-project-path" type="text" placeholder="C:\path\to\repo" style="flex:1; padding:8px 10px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); color:var(--text); font-size:var(--text-md); font-family:inherit">
304
304
  <button id="slim-add-project-browse" class="btn-secondary" type="button">Browse&hellip;</button>
305
305
  </div>
306
- <div id="slim-add-project-msg" style="margin-top:10px; font-size:12px; min-height:16px"></div>
306
+ <div id="slim-add-project-msg" style="margin-top:10px; font-size:var(--text-base); min-height:16px"></div>
307
307
  </div>
308
308
  <div class="modal-footer">
309
309
  <button id="slim-add-project-cancel" class="btn-secondary" type="button">Cancel</button>
@@ -22,8 +22,38 @@
22
22
  --amber: #d29922;
23
23
  --red: #f85149;
24
24
  --radius: 6px;
25
+
26
+ /* Typography — mirrors dashboard/styles.css :root tokens
27
+ (W-mq1c8og40003a7cf). Slim ships its CSS independently
28
+ (template-baked into slim/layout.html), so we duplicate the
29
+ token VALUES here verbatim — see dashboard/docs/typography.md.
30
+ A tripwire test asserts the slim and main tokens stay in
31
+ sync (test/unit/dashboard-font-size-tokens.test.js). */
32
+ --text-xs: 10px; --text-sm: 11px; --text-base: 12px;
33
+ --text-md: 13px; --text-lg: 14px; --text-xl: 16px; --text-2xl: 18px;
34
+ --text-stat: 22px; --text-stat-lg: 28px; --text-display: 32px;
35
+ --text-role-display: var(--text-display);
36
+ --text-heading: var(--text-2xl);
37
+ --text-subheading: var(--text-xl);
38
+ --text-body: var(--text-lg);
39
+ --text-meta: var(--text-md);
40
+ --text-caption: var(--text-base);
41
+ --text-micro: var(--text-sm);
42
+ --text-code: 0.9em;
25
43
  }
26
44
 
45
+ /* Typography utility classes — see dashboard/styles.css for the
46
+ canonical declarations; slim duplicates because the two
47
+ stylesheets are served independently. */
48
+ .text-display { font-size: var(--text-role-display); }
49
+ .text-heading { font-size: var(--text-heading); }
50
+ .text-subheading { font-size: var(--text-subheading); }
51
+ .text-body { font-size: var(--text-body); }
52
+ .text-meta { font-size: var(--text-meta); }
53
+ .text-caption { font-size: var(--text-caption); }
54
+ .text-micro { font-size: var(--text-micro); }
55
+ .text-code { font-size: var(--text-code); }
56
+
27
57
  * { box-sizing: border-box; }
28
58
 
29
59
  /* Typography baseline (slim UX foundation phase).
@@ -41,7 +71,7 @@
41
71
  background: var(--bg);
42
72
  color: var(--text);
43
73
  font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
44
- font-size: 16px;
74
+ font-size: var(--text-xl);
45
75
  line-height: 1.5;
46
76
  }
47
77
 
@@ -89,7 +119,8 @@
89
119
  }
90
120
  .topbar-title {
91
121
  font-weight: 600;
92
- font-size: 15px;
122
+ /* Rounded from 15px → 14px (--text-lg) in W-mq1c8og40003a7cf typography centralization. */
123
+ font-size: var(--text-lg);
93
124
  color: var(--text);
94
125
  letter-spacing: 0.3px;
95
126
  }
@@ -97,7 +128,7 @@
97
128
  display: inline-block;
98
129
  margin-left: 8px;
99
130
  padding: 1px 6px;
100
- font-size: 12px;
131
+ font-size: var(--text-base);
101
132
  font-weight: 700;
102
133
  letter-spacing: 0.5px;
103
134
  color: var(--blue);
@@ -125,7 +156,8 @@
125
156
  border: none;
126
157
  cursor: pointer;
127
158
  color: var(--muted);
128
- font-size: 20px;
159
+ /* Rounded from 20px → 18px (--text-2xl) in W-mq1c8og40003a7cf typography centralization. */
160
+ font-size: var(--text-2xl);
129
161
  line-height: 1;
130
162
  padding: 6px 10px;
131
163
  border-radius: var(--radius);
@@ -164,7 +196,7 @@
164
196
  }
165
197
  .panel-header {
166
198
  padding: 9px 14px;
167
- font-size: 12px;
199
+ font-size: var(--text-base);
168
200
  font-weight: 700;
169
201
  letter-spacing: 0.7px;
170
202
  text-transform: uppercase;
@@ -180,7 +212,7 @@
180
212
  letter-spacing: 0;
181
213
  font-weight: 400;
182
214
  color: var(--muted);
183
- font-size: 12px;
215
+ font-size: var(--text-base);
184
216
  }
185
217
  .panel-body {
186
218
  flex: 1;
@@ -191,7 +223,7 @@
191
223
  .panel-placeholder {
192
224
  color: var(--muted);
193
225
  font-style: italic;
194
- font-size: 14px;
226
+ font-size: var(--text-lg);
195
227
  }
196
228
 
197
229
  .panel-actions { grid-area: actions; }
@@ -223,7 +255,7 @@
223
255
  }
224
256
  .chat-empty {
225
257
  color: var(--muted);
226
- font-size: 14px;
258
+ font-size: var(--text-lg);
227
259
  font-style: italic;
228
260
  text-align: center;
229
261
  margin-top: 24px;
@@ -234,7 +266,7 @@
234
266
  border-radius: var(--radius);
235
267
  white-space: pre-wrap;
236
268
  word-wrap: break-word;
237
- font-size: 16px;
269
+ font-size: var(--text-xl);
238
270
  }
239
271
  .chat-msg.user {
240
272
  align-self: flex-end;
@@ -251,7 +283,7 @@
251
283
  align-self: center;
252
284
  background: transparent;
253
285
  color: var(--muted);
254
- font-size: 13px;
286
+ font-size: var(--text-md);
255
287
  font-style: italic;
256
288
  }
257
289
  .chat-msg.error {
@@ -259,7 +291,7 @@
259
291
  background: rgba(248, 81, 73, 0.1);
260
292
  color: var(--red);
261
293
  border: 1px solid var(--red);
262
- font-size: 14px;
294
+ font-size: var(--text-lg);
263
295
  }
264
296
  .chat-input-wrap {
265
297
  display: flex;
@@ -290,14 +322,14 @@
290
322
  max-width: 800px;
291
323
  margin-left: auto;
292
324
  margin-right: auto;
293
- font-size: 12px;
325
+ font-size: var(--text-base);
294
326
  color: var(--muted);
295
327
  }
296
328
  .chat-context-strip .context-label {
297
329
  text-transform: uppercase;
298
330
  letter-spacing: 0.5px;
299
331
  font-weight: 700;
300
- font-size: 12px;
332
+ font-size: var(--text-base);
301
333
  }
302
334
  .chat-context-strip select {
303
335
  background: var(--bg);
@@ -306,7 +338,7 @@
306
338
  border-radius: var(--radius);
307
339
  padding: 4px 8px;
308
340
  font-family: inherit;
309
- font-size: 12px;
341
+ font-size: var(--text-base);
310
342
  cursor: pointer;
311
343
  }
312
344
  .chat-context-strip select:focus { outline: none; border-color: var(--blue); }
@@ -324,7 +356,7 @@
324
356
  border: 1px solid var(--border);
325
357
  border-radius: var(--radius);
326
358
  padding: 3px 9px;
327
- font-size: 12px;
359
+ font-size: var(--text-base);
328
360
  cursor: pointer;
329
361
  line-height: 1;
330
362
  }
@@ -339,7 +371,7 @@
339
371
  color: var(--text);
340
372
  border: 1px solid var(--border);
341
373
  border-radius: var(--radius);
342
- font-size: 16px;
374
+ font-size: var(--text-xl);
343
375
  font-family: inherit;
344
376
  resize: none;
345
377
  /* >= 2 lines visible by default. 16px font * 1.5 line-height = 24px/line,
@@ -354,7 +386,7 @@
354
386
  color: #fff;
355
387
  border: none;
356
388
  border-radius: var(--radius);
357
- font-size: 14px;
389
+ font-size: var(--text-lg);
358
390
  font-weight: 600;
359
391
  cursor: pointer;
360
392
  align-self: flex-end;
@@ -367,7 +399,7 @@
367
399
  color: var(--red);
368
400
  border: 1px solid var(--border);
369
401
  border-radius: var(--radius);
370
- font-size: 13px;
402
+ font-size: var(--text-md);
371
403
  cursor: pointer;
372
404
  align-self: flex-end;
373
405
  display: none;
@@ -413,7 +445,7 @@
413
445
  }
414
446
  .chat-tool {
415
447
  color: var(--muted);
416
- font-size: 12px;
448
+ font-size: var(--text-base);
417
449
  line-height: 1.5;
418
450
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
419
451
  white-space: nowrap;
@@ -425,7 +457,7 @@
425
457
  /* Tool-calls modal: full list, wrapped so long commands are readable. */
426
458
  .tools-modal-line {
427
459
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
428
- font-size: 12px;
460
+ font-size: var(--text-base);
429
461
  color: var(--text);
430
462
  padding: 5px 0;
431
463
  border-bottom: 1px solid var(--border);
@@ -434,7 +466,7 @@
434
466
  }
435
467
  .tools-modal-line::before { content: "● "; color: var(--muted); }
436
468
  .tools-modal-line:last-child { border-bottom: none; }
437
- .tools-modal-empty { color: var(--muted); font-style: italic; font-size: 13px; }
469
+ .tools-modal-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); }
438
470
  /* Wide enough to span the Actions + Status columns (820 + 10 gap + 548). */
439
471
  #slim-tools-modal .modal {
440
472
  width: 1378px;
@@ -458,13 +490,13 @@
458
490
  a.tile-item:hover { border-color: var(--blue); }
459
491
  .tile-item-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
460
492
  .tile-item-title {
461
- font-size: 14px; font-weight: 600; color: var(--text);
493
+ font-size: var(--text-lg); font-weight: 600; color: var(--text);
462
494
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
463
495
  }
464
- .tile-item-meta { font-size: 12px; color: var(--muted); margin-top: 3px; word-break: break-word; }
496
+ .tile-item-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; word-break: break-word; }
465
497
  .tile-chip {
466
498
  flex: 0 0 auto;
467
- font-size: 12px; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
499
+ font-size: var(--text-base); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
468
500
  padding: 1px 7px; border-radius: 10px;
469
501
  background: var(--surface); border: 1px solid var(--border); color: var(--muted);
470
502
  }
@@ -472,7 +504,7 @@
472
504
  .tile-chip.amber { background: rgba(210, 153, 34, 0.15); color: var(--amber); border-color: var(--amber); }
473
505
  .tile-chip.red { background: rgba(248, 81, 73, 0.15); color: var(--red); border-color: var(--red); }
474
506
  .tile-chip.blue { background: rgba(88, 166, 255, 0.12); color: var(--blue); border-color: var(--blue); }
475
- .tile-empty { color: var(--muted); font-style: italic; font-size: 13px; }
507
+ .tile-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); }
476
508
 
477
509
  /* Pinned-context list rows (slim-pinned-modal). */
478
510
  .pinned-row {
@@ -499,7 +531,7 @@
499
531
 
500
532
  .chat-thinking {
501
533
  color: var(--muted);
502
- font-size: 13px;
534
+ font-size: var(--text-md);
503
535
  font-style: italic;
504
536
  }
505
537
  .chat-thinking-dots {
@@ -526,7 +558,7 @@
526
558
  align-self: flex-start;
527
559
  padding: 5px 11px;
528
560
  border-radius: 4px;
529
- font-size: 12px;
561
+ font-size: var(--text-base);
530
562
  border: 1px dashed var(--border);
531
563
  color: var(--muted);
532
564
  }
@@ -543,14 +575,14 @@
543
575
  background: var(--bg);
544
576
  padding: 1px 4px;
545
577
  border-radius: 3px;
546
- font-size: 0.9em;
578
+ font-size: var(--text-code);
547
579
  }
548
580
  .chat-msg.assistant pre {
549
581
  background: var(--bg);
550
582
  padding: 10px;
551
583
  border-radius: 4px;
552
584
  overflow-x: auto;
553
- font-size: 14px;
585
+ font-size: var(--text-lg);
554
586
  margin: 6px 0;
555
587
  }
556
588
  .chat-msg.assistant pre code { background: none; padding: 0; }
@@ -577,7 +609,7 @@
577
609
  border-radius: var(--radius);
578
610
  padding: 10px 12px;
579
611
  cursor: pointer;
580
- font-size: 13px;
612
+ font-size: var(--text-md);
581
613
  font-family: inherit;
582
614
  display: flex;
583
615
  flex-direction: column;
@@ -602,28 +634,28 @@
602
634
  .act-ic-link { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%3Cpath%20d%3D%22M9%207C9.55228%207%2010%207.44772%2010%208C10%208.51284%209.61396%208.93551%209.11662%208.99327L9%209H7C5.34315%209%204%2010.3431%204%2012C4%2013.5977%205.24892%2014.9037%206.82373%2014.9949L7%2015H9C9.55228%2015%2010%2015.4477%2010%2016C10%2016.5128%209.61396%2016.9355%209.11662%2016.9933L9%2017H7C4.23858%2017%202%2014.7614%202%2012C2%209.31125%204.12231%207.11818%206.78311%207.00462L7%207H9ZM17%207C19.7614%207%2022%209.23858%2022%2012C22%2014.6888%2019.8777%2016.8818%2017.2169%2016.9954L17%2017H15C14.4477%2017%2014%2016.5523%2014%2016C14%2015.4872%2014.386%2015.0645%2014.8834%2015.0067L15%2015H17C18.6569%2015%2020%2013.6569%2020%2012C20%2010.4023%2018.7511%209.09634%2017.1763%209.00509L17%209H15C14.4477%209%2014%208.55228%2014%208C14%207.48716%2014.386%207.06449%2014.8834%207.00673L15%207H17ZM7%2011H17C17.5523%2011%2018%2011.4477%2018%2012C18%2012.5128%2017.614%2012.9355%2017.1166%2012.9933L17%2013H7C6.44772%2013%206%2012.5523%206%2012C6%2011.4872%206.38604%2011.0645%206.88338%2011.0067L7%2011H17H7Z%22%20fill%3D%22%23212121%22%2F%3E%20%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%3Cpath%20d%3D%22M9%207C9.55228%207%2010%207.44772%2010%208C10%208.51284%209.61396%208.93551%209.11662%208.99327L9%209H7C5.34315%209%204%2010.3431%204%2012C4%2013.5977%205.24892%2014.9037%206.82373%2014.9949L7%2015H9C9.55228%2015%2010%2015.4477%2010%2016C10%2016.5128%209.61396%2016.9355%209.11662%2016.9933L9%2017H7C4.23858%2017%202%2014.7614%202%2012C2%209.31125%204.12231%207.11818%206.78311%207.00462L7%207H9ZM17%207C19.7614%207%2022%209.23858%2022%2012C22%2014.6888%2019.8777%2016.8818%2017.2169%2016.9954L17%2017H15C14.4477%2017%2014%2016.5523%2014%2016C14%2015.4872%2014.386%2015.0645%2014.8834%2015.0067L15%2015H17C18.6569%2015%2020%2013.6569%2020%2012C20%2010.4023%2018.7511%209.09634%2017.1763%209.00509L17%209H15C14.4477%209%2014%208.55228%2014%208C14%207.48716%2014.386%207.06449%2014.8834%207.00673L15%207H17ZM7%2011H17C17.5523%2011%2018%2011.4477%2018%2012C18%2012.5128%2017.614%2012.9355%2017.1166%2012.9933L17%2013H7C6.44772%2013%206%2012.5523%206%2012C6%2011.4872%206.38604%2011.0645%206.88338%2011.0067L7%2011H17H7Z%22%20fill%3D%22%23212121%22%2F%3E%20%3C%2Fsvg%3E"); }
603
635
  .act-ic-pin { -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%3Cpath%20d%3D%22M21.0682%207.75765L16.2425%202.93189C14.9152%201.60462%2012.6777%201.96772%2011.8382%203.6466L9.40281%208.51748C9.31512%208.69287%209.16223%208.82694%208.97688%208.89096L4.81061%2010.3302C3.93791%2010.6317%203.682%2011.7427%204.33487%2012.3956L7.43936%2015.5001L3.00008%2019.9394L3%2021.0001H4.06074L8.50002%2016.5607L11.6045%2019.6653C12.2574%2020.3181%2013.3684%2020.0622%2013.6699%2019.1895L15.1092%2015.0232C15.1732%2014.8379%2015.3073%2014.685%2015.4826%2014.5973L20.3535%2012.1619C22.0324%2011.3224%2022.3955%209.08491%2021.0682%207.75765Z%22%20fill%3D%22%23212121%22%2F%3E%20%3C%2Fsvg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%3Cpath%20d%3D%22M21.0682%207.75765L16.2425%202.93189C14.9152%201.60462%2012.6777%201.96772%2011.8382%203.6466L9.40281%208.51748C9.31512%208.69287%209.16223%208.82694%208.97688%208.89096L4.81061%2010.3302C3.93791%2010.6317%203.682%2011.7427%204.33487%2012.3956L7.43936%2015.5001L3.00008%2019.9394L3%2021.0001H4.06074L8.50002%2016.5607L11.6045%2019.6653C12.2574%2020.3181%2013.3684%2020.0622%2013.6699%2019.1895L15.1092%2015.0232C15.1732%2014.8379%2015.3073%2014.685%2015.4826%2014.5973L20.3535%2012.1619C22.0324%2011.3224%2022.3955%209.08491%2021.0682%207.75765Z%22%20fill%3D%22%23212121%22%2F%3E%20%3C%2Fsvg%3E"); }
604
636
  .link-pr-btn .link-pr-label { font-weight: 600; }
605
- .link-pr-btn .link-pr-sub { font-size: 12px; color: var(--muted); }
637
+ .link-pr-btn .link-pr-sub { font-size: var(--text-base); color: var(--muted); }
606
638
 
607
639
  /* Link-PR dialog fields + the "+ Link PR" chip (tile corner + modal header). */
608
- .linkpr-label { display: block; color: var(--text); font-size: 13px; margin-bottom: 10px; }
640
+ .linkpr-label { display: block; color: var(--text); font-size: var(--text-md); margin-bottom: 10px; }
609
641
  .linkpr-input {
610
642
  display: block; width: 100%; margin-top: 4px; padding: 7px 9px;
611
643
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
612
- color: var(--text); font-size: 13px; font-family: inherit; resize: vertical;
644
+ color: var(--text); font-size: var(--text-md); font-family: inherit; resize: vertical;
613
645
  }
614
646
  .linkpr-input:focus { outline: none; border-color: var(--blue); }
615
- .linkpr-check { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: 13px; cursor: pointer; margin-bottom: 4px; }
647
+ .linkpr-check { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: var(--text-md); cursor: pointer; margin-bottom: 4px; }
616
648
  .linkpr-check input { width: 16px; height: 16px; accent-color: var(--blue); }
617
649
  .linkpr-hint-inline { color: var(--muted); font-weight: 400; }
618
- .linkpr-hint { font-size: 12px; color: var(--muted); padding-left: 24px; }
650
+ .linkpr-hint { font-size: var(--text-base); color: var(--muted); padding-left: 24px; }
619
651
  .linkpr-warning {
620
652
  padding: 8px 12px; margin-bottom: 10px;
621
653
  background: rgba(210, 153, 34, 0.15); border: 1px solid rgba(210, 153, 34, 0.3);
622
- border-radius: var(--radius); font-size: 13px; line-height: 1.5; color: var(--text);
654
+ border-radius: var(--radius); font-size: var(--text-md); line-height: 1.5; color: var(--text);
623
655
  }
624
656
  .linkpr-warning-title { font-weight: 700; margin-bottom: 2px; color: var(--amber); }
625
657
  .linkpr-chip {
626
- font-size: 12px; font-weight: 600;
658
+ font-size: var(--text-base); font-weight: 600;
627
659
  padding: 1px 8px; border-radius: 10px;
628
660
  background: rgba(63, 185, 80, 0.12); color: var(--green); border: 1px solid var(--green);
629
661
  cursor: pointer; font-family: inherit;
@@ -671,7 +703,7 @@
671
703
  background: rgba(248, 81, 73, 0.08);
672
704
  }
673
705
  .cockpit-label {
674
- font-size: 12px;
706
+ font-size: var(--text-base);
675
707
  font-weight: 700;
676
708
  letter-spacing: 0.6px;
677
709
  text-transform: uppercase;
@@ -692,14 +724,14 @@
692
724
  .lit-amber .cockpit-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
693
725
  .lit-red .cockpit-dot { background: var(--red); box-shadow: 0 0 6px var(--red); }
694
726
  .cockpit-value {
695
- font-size: 22px;
727
+ font-size: var(--text-stat);
696
728
  font-weight: 700;
697
729
  color: var(--text);
698
730
  line-height: 1;
699
731
  }
700
732
  .cockpit-value.dim { color: var(--muted); }
701
733
  .cockpit-detail {
702
- font-size: 12px;
734
+ font-size: var(--text-base);
703
735
  color: var(--muted);
704
736
  line-height: 1.3;
705
737
  min-height: 14px;
@@ -726,7 +758,7 @@
726
758
  .member-empty {
727
759
  grid-column: 1 / -1;
728
760
  color: var(--muted);
729
- font-size: 13px;
761
+ font-size: var(--text-md);
730
762
  font-style: italic;
731
763
  }
732
764
  .member-card {
@@ -750,9 +782,10 @@
750
782
  .member-card.working { border-color: rgba(210, 153, 34, 0.6); }
751
783
  .member-card.done { border-color: rgba(63, 185, 80, 0.6); }
752
784
  .member-card.error { border-color: rgba(248, 81, 73, 0.6); }
753
- .member-emoji { font-size: 24px; line-height: 1; }
785
+ /* Rounded from 24px → 22px (--text-stat) in W-mq1c8og40003a7cf typography centralization. Emoji glyph; 2px diff is visually negligible. */
786
+ .member-emoji { font-size: var(--text-stat); line-height: 1; }
754
787
  .member-name {
755
- font-size: 13px;
788
+ font-size: var(--text-md);
756
789
  font-weight: 600;
757
790
  line-height: 1.2;
758
791
  color: var(--text);
@@ -762,7 +795,7 @@
762
795
  white-space: nowrap;
763
796
  }
764
797
  .member-role {
765
- font-size: 12px;
798
+ font-size: var(--text-base);
766
799
  line-height: 1.2;
767
800
  color: var(--muted);
768
801
  max-width: 100%;
@@ -774,7 +807,7 @@
774
807
  status-badge but tuned to the slim token palette. */
775
808
  .member-status {
776
809
  margin-top: 2px;
777
- font-size: 12px;
810
+ font-size: var(--text-base);
778
811
  font-weight: 700;
779
812
  letter-spacing: 0.5px;
780
813
  text-transform: uppercase;
@@ -812,19 +845,21 @@
812
845
  gap: 12px;
813
846
  margin-bottom: 14px;
814
847
  }
815
- .agent-detail-emoji { font-size: 34px; line-height: 1; }
816
- .agent-detail-name { font-size: 17px; font-weight: 600; color: var(--text); }
817
- .agent-detail-role { font-size: 13px; color: var(--muted); }
848
+ /* Rounded from 34px → 32px (--text-display) in W-mq1c8og40003a7cf typography centralization. Emoji glyph; 2px diff is visually negligible. */
849
+ .agent-detail-emoji { font-size: var(--text-display); line-height: 1; }
850
+ /* Rounded from 17px 18px (--text-2xl) in W-mq1c8og40003a7cf typography centralization. */
851
+ .agent-detail-name { font-size: var(--text-2xl); font-weight: 600; color: var(--text); }
852
+ .agent-detail-role { font-size: var(--text-md); color: var(--muted); }
818
853
  .agent-detail-row { margin-bottom: 12px; }
819
854
  .agent-detail-key {
820
- font-size: 12px;
855
+ font-size: var(--text-base);
821
856
  font-weight: 700;
822
857
  letter-spacing: 0.5px;
823
858
  text-transform: uppercase;
824
859
  color: var(--muted);
825
860
  margin-bottom: 3px;
826
861
  }
827
- .agent-detail-val { font-size: 13px; color: var(--text); white-space: pre-wrap; word-break: break-word; }
862
+ .agent-detail-val { font-size: var(--text-md); color: var(--text); white-space: pre-wrap; word-break: break-word; }
828
863
  .agent-detail-val.muted { color: var(--muted); font-style: italic; }
829
864
 
830
865
  /* ── History panel: event log ─────────────────────────────── */
@@ -851,27 +886,27 @@
851
886
  justify-content: space-between;
852
887
  align-items: baseline;
853
888
  gap: 8px;
854
- font-size: 13px;
889
+ font-size: var(--text-md);
855
890
  }
856
891
  .history-kind {
857
892
  font-weight: 700;
858
893
  letter-spacing: 0.4px;
859
894
  text-transform: uppercase;
860
- font-size: 12px;
895
+ font-size: var(--text-base);
861
896
  color: var(--muted);
862
897
  }
863
898
  .history-time {
864
- font-size: 12px;
899
+ font-size: var(--text-base);
865
900
  color: var(--muted);
866
901
  white-space: nowrap;
867
902
  }
868
903
  .history-title {
869
- font-size: 13px;
904
+ font-size: var(--text-md);
870
905
  color: var(--text);
871
906
  word-break: break-word;
872
907
  }
873
908
  .history-meta {
874
- font-size: 12px;
909
+ font-size: var(--text-base);
875
910
  color: var(--muted);
876
911
  word-break: break-word;
877
912
  }
@@ -879,7 +914,7 @@
879
914
  .history-empty {
880
915
  color: var(--muted);
881
916
  font-style: italic;
882
- font-size: 13px;
917
+ font-size: var(--text-md);
883
918
  }
884
919
 
885
920
  /* ╔══════════════════════════════════════════════════════════════════╗
@@ -941,7 +976,7 @@
941
976
  justify-content: center;
942
977
  gap: 4px;
943
978
  color: var(--muted);
944
- font-size: 12px;
979
+ font-size: var(--text-base);
945
980
  }
946
981
  .completions-card-body-top {
947
982
  /* parent grid cell (col 2, row 1) — wraps title and optional
@@ -958,7 +993,7 @@
958
993
  relative time so the rail's bottom row reads as a single quiet line:
959
994
  `✓ · 8h ago`. */
960
995
  .completions-card-status-icon {
961
- font-size: 12px;
996
+ font-size: var(--text-base);
962
997
  line-height: 1;
963
998
  color: var(--muted);
964
999
  }
@@ -1036,7 +1071,7 @@
1036
1071
  but a larger font so it reads as the rail's primary type marker. */
1037
1072
  .completions-card-type-word {
1038
1073
  font-family: 'Consolas', 'Courier New', monospace;
1039
- font-size: 14px;
1074
+ font-size: var(--text-lg);
1040
1075
  font-weight: 700;
1041
1076
  letter-spacing: 0.5px;
1042
1077
  line-height: 1;
@@ -1049,12 +1084,12 @@
1049
1084
  cursor: help;
1050
1085
  }
1051
1086
  .completions-card-time {
1052
- font-size: 12px;
1087
+ font-size: var(--text-base);
1053
1088
  color: var(--muted);
1054
1089
  white-space: nowrap;
1055
1090
  }
1056
1091
  .completions-card-title {
1057
- font-size: 16px;
1092
+ font-size: var(--text-xl);
1058
1093
  color: var(--text);
1059
1094
  line-height: 1.35;
1060
1095
  /* Always reserve two lines so cards have a consistent vertical
@@ -1070,7 +1105,7 @@
1070
1105
  /* Byline — "Agent · Role" under the title. Quiet (muted, small) so the
1071
1106
  title stays primary, but always present so every row names its owner. */
1072
1107
  .completions-card-byline {
1073
- font-size: 12px;
1108
+ font-size: var(--text-base);
1074
1109
  color: var(--muted);
1075
1110
  white-space: nowrap;
1076
1111
  overflow: hidden;
@@ -1078,7 +1113,7 @@
1078
1113
  min-width: 0;
1079
1114
  }
1080
1115
  .completions-card-fail-line {
1081
- font-size: 12px;
1116
+ font-size: var(--text-base);
1082
1117
  color: var(--red);
1083
1118
  word-break: break-word;
1084
1119
  }
@@ -1092,7 +1127,7 @@
1092
1127
  above the chips is supplied by the parent card grid's row gap. */
1093
1128
  .completions-card-lineage {
1094
1129
  grid-column: 2; /* cell 2,2 of the parent card grid */
1095
- font-size: 12px;
1130
+ font-size: var(--text-base);
1096
1131
  color: var(--muted);
1097
1132
  display: grid;
1098
1133
  grid-template-columns: 1fr 1fr 1fr;
@@ -1101,7 +1136,7 @@
1101
1136
  }
1102
1137
  .completions-card-chip {
1103
1138
  font-family: 'Consolas', 'Courier New', monospace;
1104
- font-size: 12px;
1139
+ font-size: var(--text-base);
1105
1140
  background: var(--bg);
1106
1141
  border: 1px solid var(--border);
1107
1142
  border-radius: 3px;
@@ -1146,7 +1181,7 @@
1146
1181
  .completions-modal-section { margin-bottom: 14px; }
1147
1182
  .completions-modal-section:last-child { margin-bottom: 0; }
1148
1183
  .completions-modal-label {
1149
- font-size: 12px;
1184
+ font-size: var(--text-base);
1150
1185
  font-weight: 700;
1151
1186
  letter-spacing: 0.4px;
1152
1187
  text-transform: uppercase;
@@ -1154,14 +1189,14 @@
1154
1189
  margin-bottom: 4px;
1155
1190
  }
1156
1191
  .completions-modal-value {
1157
- font-size: 13px;
1192
+ font-size: var(--text-md);
1158
1193
  color: var(--text);
1159
1194
  word-break: break-word;
1160
1195
  white-space: pre-wrap;
1161
1196
  }
1162
1197
  .completions-modal-report {
1163
1198
  font-family: 'Consolas', 'Courier New', monospace;
1164
- font-size: 12px;
1199
+ font-size: var(--text-base);
1165
1200
  background: var(--bg);
1166
1201
  border: 1px solid var(--border);
1167
1202
  border-radius: var(--radius);
@@ -1209,9 +1244,9 @@
1209
1244
  padding: 12px 16px;
1210
1245
  border-bottom: 1px solid var(--border);
1211
1246
  }
1212
- .modal-header h3 { margin: 0; font-size: 16px; color: var(--blue); }
1247
+ .modal-header h3 { margin: 0; font-size: var(--text-xl); color: var(--blue); }
1213
1248
  .modal-body { padding: 14px 18px; overflow-y: auto; }
1214
- .modal-body p { color: var(--muted); margin: 4px 0 12px; font-size: 13px; }
1249
+ .modal-body p { color: var(--muted); margin: 4px 0 12px; font-size: var(--text-md); }
1215
1250
  .modal-footer {
1216
1251
  padding: 10px 16px;
1217
1252
  border-top: 1px solid var(--border);
@@ -1226,7 +1261,7 @@
1226
1261
  border: none;
1227
1262
  border-radius: var(--radius);
1228
1263
  cursor: pointer;
1229
- font-size: 13px;
1264
+ font-size: var(--text-md);
1230
1265
  font-weight: 600;
1231
1266
  }
1232
1267
  .btn-secondary {
@@ -1236,7 +1271,7 @@
1236
1271
  border: 1px solid var(--border);
1237
1272
  border-radius: var(--radius);
1238
1273
  cursor: pointer;
1239
- font-size: 13px;
1274
+ font-size: var(--text-md);
1240
1275
  }
1241
1276
  .btn-primary:hover, .btn-secondary:hover { filter: brightness(1.1); }
1242
1277
 
@@ -1248,9 +1283,9 @@
1248
1283
  border-bottom: 1px solid var(--border);
1249
1284
  }
1250
1285
  .flag-row:last-child { border-bottom: none; }
1251
- .flag-name { font-size: 14px; font-weight: 600; }
1252
- .flag-desc { font-size: 12px; color: var(--muted); margin-top: 2px; }
1253
- .flag-toggle { display: flex; align-items: center; gap: 8px; font-size: 13px; }
1286
+ .flag-name { font-size: var(--text-lg); font-weight: 600; }
1287
+ .flag-desc { font-size: var(--text-base); color: var(--muted); margin-top: 2px; }
1288
+ .flag-toggle { display: flex; align-items: center; gap: 8px; font-size: var(--text-md); }
1254
1289
  .flag-toggle input { cursor: pointer; }
1255
1290
  .settings-link {
1256
1291
  display: block;
@@ -1261,7 +1296,7 @@
1261
1296
  border-radius: var(--radius);
1262
1297
  color: var(--blue);
1263
1298
  text-decoration: none;
1264
- font-size: 13px;
1299
+ font-size: var(--text-md);
1265
1300
  text-align: center;
1266
1301
  }
1267
1302
  .settings-link:hover { filter: brightness(1.1); }
@@ -1274,7 +1309,7 @@
1274
1309
  color: var(--text);
1275
1310
  border: 1px solid var(--border);
1276
1311
  border-radius: var(--radius);
1277
- font-size: 14px;
1312
+ font-size: var(--text-lg);
1278
1313
  font-family: inherit;
1279
1314
  resize: vertical;
1280
1315
  }
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2150",
3
+ "version": "0.1.2151",
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",