@dadado/agent-kit-cli 4.8.9 → 5.1.0

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.
@@ -19,6 +19,21 @@
19
19
  var fav = document.getElementById('favicon');
20
20
  if (fav) fav.setAttribute('href', skin === 'cursor' ? '/logo-cursor.svg' : '/logo.svg');
21
21
  })();
22
+ /* Restore Crew Monitor row density before first paint; auto-pick by viewport
23
+ when the operator has no stored preference. Same namespaced-key + blocked-
24
+ storage tolerance as the skin restore above. */
25
+ (function () {
26
+ var key = 'agent-kit:monitor-density';
27
+ var density = null;
28
+ try {
29
+ var stored = localStorage.getItem(key);
30
+ if (stored === 'compact' || stored === 'comfortable') density = stored;
31
+ } catch (e) { /* private mode / blocked storage */ }
32
+ if (!density) {
33
+ density = (window.innerWidth || 0) >= 900 ? 'comfortable' : 'compact';
34
+ }
35
+ document.documentElement.setAttribute('data-monitor-density', density);
36
+ })();
22
37
  </script>
23
38
  <style>
24
39
  /* ===== Reset & Base ===== */
@@ -34,7 +49,7 @@ html[data-dashboard-skin="legacy"] {
34
49
  --border-active: #2a3a5a;
35
50
  --text-primary: #e2e8f0;
36
51
  --text-secondary: #8899aa;
37
- --text-muted: #6d8094;
52
+ --text-muted: #6d8094; /* landing uses #7f93a8 (AA over marketing gradient); see docs/design-system.md */
38
53
  --green: #22c55e;
39
54
  --green-bg: rgba(34,197,94,0.12);
40
55
  --yellow: #eab308;
@@ -1026,7 +1041,6 @@ body.mc-fullscreen .top-tabs-row {
1026
1041
  .health-item-sev[data-sev="ok"] { color: var(--green); }
1027
1042
  .health-item-sev[data-sev="warning"] { color: var(--yellow); }
1028
1043
  .health-item-sev[data-sev="degraded"] { color: var(--orange); }
1029
- .health-item-sev[data-sev="error"] { color: var(--red); }
1030
1044
  .health-item-chevron {
1031
1045
  color: var(--text-muted);
1032
1046
  font-size: 10px;
@@ -1700,11 +1714,20 @@ body.mc-fullscreen .top-tabs-row {
1700
1714
  white-space: nowrap;
1701
1715
  }
1702
1716
 
1703
- /* ===== Monitor feed: single flat rows (Phase 1) ===== */
1717
+ /* ===== Crew Monitor row (design v2, 2026-08-05) =====
1718
+ Ported from the Claude Design project `Mission Control — Crew Monitor`
1719
+ (verdict: .cursor/context/mission-control-design/remote/v1/ACCEPTANCE.md).
1720
+
1721
+ The layout contract that replaced the equal-shrink segment row: exactly ONE
1722
+ flexible field per row. `.monitor-row-primary` is the only field allowed to
1723
+ ellipsis; every other column is fixed-width or fixed-content, and the ref
1724
+ chips hide WHOLE at a breakpoint rather than truncating. That is what stops
1725
+ `revi…` / `8f…` / `P…` from coming back — a field that cannot show one whole
1726
+ token is removed instead of shrunk. */
1704
1727
  .live-activity-feed .monitor-row {
1705
1728
  display: flex;
1706
1729
  align-items: center;
1707
- gap: 10px;
1730
+ gap: 8px;
1708
1731
  padding: 6px 12px;
1709
1732
  font-size: 12px;
1710
1733
  color: var(--text-secondary);
@@ -1716,90 +1739,195 @@ body.mc-fullscreen .top-tabs-row {
1716
1739
  .live-activity-feed .monitor-row:last-child {
1717
1740
  padding-bottom: 10px;
1718
1741
  }
1719
- .live-activity-feed .monitor-row .monitor-row-chip {
1720
- display: inline-flex;
1721
- align-items: center;
1722
- justify-content: center;
1742
+ /* Team badge at row start (design v2 identity pass). Replaces the kind glyph:
1743
+ the glyph's two jobs split, so colour lands here (kind tint + solid semantic
1744
+ initials) and meaning lands on the badge's tooltip gloss. Unlike the avatar
1745
+ box removed in #631/#637-#639, this is not a decorative identity chip — it is
1746
+ the kind cue and the actor cue in one 18px square, which is why `info.bg`
1747
+ renders on a Crew row again. Stays at --mc-radius-sm while the status pills
1748
+ sit at --mc-radius-chrome, so a badge never reads as a status pill. */
1749
+ .live-activity-feed .monitor-row .monitor-row-badge {
1750
+ flex: 0 0 18px;
1723
1751
  width: 18px;
1724
- min-width: 18px;
1725
1752
  height: 18px;
1726
- padding: 0;
1727
- border-radius: 4px;
1728
- font-size: 10px;
1729
- font-weight: 700;
1730
- flex-shrink: 0;
1731
- line-height: 1;
1732
- box-sizing: border-box;
1733
- /* Solid *-bg fill only; no stroke / inset ring / left rail */
1734
- border: none;
1735
- box-shadow: none;
1736
- }
1737
- .live-activity-feed .monitor-row .monitor-row-icon {
1738
- font-size: 11px;
1739
- line-height: 1;
1740
- width: 1em;
1741
- text-align: center;
1742
- }
1743
- .live-activity-feed .monitor-row .monitor-row-avatar {
1753
+ border-radius: var(--mc-radius-sm);
1744
1754
  display: inline-flex;
1745
1755
  align-items: center;
1746
1756
  justify-content: center;
1747
- width: 18px;
1748
- min-width: 18px;
1749
- height: 18px;
1750
- border-radius: 4px;
1751
- font-size: 9px;
1757
+ font-size: 7.5px;
1752
1758
  font-weight: 700;
1759
+ letter-spacing: 0.03em;
1753
1760
  line-height: 1;
1754
- flex-shrink: 0;
1755
- background: var(--bg-card-hover);
1756
- color: var(--text-secondary);
1761
+ user-select: none;
1757
1762
  }
1758
- .live-activity-feed .monitor-row .feed-label {
1759
- flex: 1;
1763
+ /* Actor column: fixed basis, not a min-width floor. The floor let long kit
1764
+ agent ids push the verb and metadata columns off their x; a hard basis keeps
1765
+ the scan column stable across every kind. Sized so the longest mask in the
1766
+ operator lexicon ("Project Manager", "Product Analyst") fits unclipped. */
1767
+ .live-activity-feed .monitor-row .monitor-row-actor {
1768
+ flex: 0 0 9.5em;
1760
1769
  min-width: 0;
1761
1770
  overflow: hidden;
1762
1771
  text-overflow: ellipsis;
1763
1772
  white-space: nowrap;
1764
- color: var(--text-secondary);
1765
- font-size: 12px;
1766
- font-weight: 400;
1767
- display: flex;
1768
- align-items: baseline;
1769
- gap: 6px;
1773
+ font-family: var(--mc-font-mono);
1774
+ font-size: 10.5px;
1775
+ color: var(--text-muted);
1770
1776
  }
1771
- /* Structured label spans: actor + verb never shrink; the plan filename is the
1772
- low-signal segment and ellipsises first. */
1773
- .live-activity-feed .monitor-row .feed-seg {
1774
- flex-shrink: 0;
1777
+ /* Verb column: state icon + word, both inheriting the verb-state colour. */
1778
+ .live-activity-feed .monitor-row .monitor-row-verb {
1779
+ flex: 0 0 6.5em;
1780
+ display: inline-flex;
1781
+ align-items: center;
1782
+ gap: 4px;
1775
1783
  white-space: nowrap;
1776
- }
1777
- .live-activity-feed .monitor-row .feed-sep {
1778
- flex-shrink: 0;
1784
+ font-size: 11px;
1779
1785
  color: var(--text-muted);
1780
1786
  }
1781
- .live-activity-feed .monitor-row .feed-seg-mid {
1782
- flex-shrink: 1;
1787
+ .live-activity-feed .monitor-row .monitor-row-verb em {
1788
+ font-style: normal;
1789
+ }
1790
+ /* 12px against the chrome set's 16px, so the icon reads as a state mark inside
1791
+ a text column rather than as chrome. The heavier stroke is optical
1792
+ compensation: 1.5 at 12px renders lighter than 1.5 at 16px. */
1793
+ .live-activity-feed .monitor-row .monitor-row-verb-icon {
1794
+ width: 12px;
1795
+ height: 12px;
1796
+ flex-shrink: 0;
1797
+ stroke-width: 1.6;
1798
+ }
1799
+ .live-activity-feed .monitor-row .v-running { color: var(--green); }
1800
+ .live-activity-feed .monitor-row .v-awaiting { color: var(--yellow); }
1801
+ .live-activity-feed .monitor-row .v-failed { color: var(--red); }
1802
+ .live-activity-feed .monitor-row .v-failed em { font-weight: 600; }
1803
+ .live-activity-feed .monitor-row .v-merged { color: var(--cyan); }
1804
+ /* The row's only flexible field, and the only one at --text-primary: the to-do
1805
+ id / commit subject / gate is what distinguishes one row from the next. */
1806
+ .live-activity-feed .monitor-row .monitor-row-primary {
1807
+ flex: 1 1 auto;
1783
1808
  min-width: 0;
1784
1809
  overflow: hidden;
1785
1810
  text-overflow: ellipsis;
1811
+ white-space: nowrap;
1812
+ color: var(--text-primary);
1786
1813
  }
1787
- .live-activity-feed .monitor-row .feed-seg-plan {
1788
- flex-shrink: 3;
1814
+ .live-activity-feed .monitor-row .monitor-row-meta {
1815
+ flex: 0 0 auto;
1816
+ display: inline-flex;
1817
+ align-items: center;
1818
+ gap: 6px;
1789
1819
  min-width: 0;
1790
- overflow: hidden;
1791
- text-overflow: ellipsis;
1820
+ }
1821
+ /* Refs left the flexible text and became fixed-content chips (PR #NNN -> #NNN,
1822
+ 7-8 char SHA, subagent worker id). A chip is either whole or hidden. */
1823
+ .live-activity-feed .monitor-row .monitor-row-ref {
1824
+ font-family: var(--mc-font-mono);
1825
+ font-size: 10px;
1792
1826
  color: var(--text-muted);
1827
+ background: var(--bg-secondary);
1828
+ border: 1px solid var(--border);
1829
+ border-radius: 3px;
1830
+ padding: 1px 5px;
1831
+ line-height: 1.3;
1832
+ white-space: nowrap;
1793
1833
  }
1794
- .live-activity-feed .monitor-row .feed-time {
1834
+ /* Plan ref: flag + basename. Driven by refs.plan, so it renders even when the
1835
+ label omitted the segment (truncation-safe: never sniffed from the string). */
1836
+ .live-activity-feed .monitor-row .monitor-row-plan {
1837
+ display: inline-flex;
1838
+ align-items: center;
1839
+ gap: 4px;
1795
1840
  color: var(--text-muted);
1841
+ min-width: 0;
1842
+ }
1843
+ .live-activity-feed .monitor-row .monitor-row-plan-flag {
1844
+ width: 11px;
1845
+ height: 11px;
1846
+ flex-shrink: 0;
1847
+ stroke-width: 1.6;
1848
+ }
1849
+ /* Fixed content, like every other chip: no max-width, no ellipsis. The design
1850
+ capped this at 16em (24em comfortable) and kept ellipsis on it, which clipped
1851
+ real basenames — `crew-monitor-compact-labels-realtime-activity` needs 271px
1852
+ against a 240px cap, so the plan chip was rendering the same fragment the
1853
+ redesign exists to remove. It drops WHOLE at ≤720px instead; above that it
1854
+ takes the width it needs and the primary field absorbs the difference, which
1855
+ is what the row's one flexible field is for. */
1856
+ .live-activity-feed .monitor-row .monitor-row-plan-name {
1796
1857
  font-family: var(--mc-font-mono);
1797
1858
  font-size: 10px;
1798
1859
  white-space: nowrap;
1799
- min-width: 40px;
1800
- flex-shrink: 0;
1860
+ }
1861
+ .live-activity-feed .monitor-row .monitor-row-time {
1862
+ flex: 0 0 44px;
1863
+ text-align: right;
1864
+ font-family: var(--mc-font-mono);
1865
+ font-size: 10px;
1866
+ color: var(--text-muted);
1867
+ white-space: nowrap;
1868
+ }
1869
+
1870
+ /* ===== Crew Monitor compaction ladder =====
1871
+ Container queries on the feed, not viewport media queries: the Crew Monitor
1872
+ is a panel inside a 2x2 cockpit grid and a narrow plugin shell, so the width
1873
+ that matters is the feed's own, never the window's.
1874
+ Every step hides a field WHOLE. Nothing here narrows a column and leaves it
1875
+ ellipsising — the actor drops out entirely rather than becoming `Project
1876
+ Man…` (delta A in ACCEPTANCE.md; the design's ladder narrowed it instead).
1877
+ Identity survives the drop: the badge keeps the initials, and the actor's own
1878
+ title plus the row tooltip still name the role in full. */
1879
+ .monitor-cq {
1880
+ container-type: inline-size;
1881
+ }
1882
+ @container (max-width: 720px) {
1883
+ .live-activity-feed .monitor-row .monitor-row-plan-name { display: none; }
1884
+ }
1885
+ @container (max-width: 560px) {
1886
+ .live-activity-feed .monitor-row .monitor-row-ref.ref-sha { display: none; }
1887
+ .live-activity-feed .monitor-row .monitor-row-actor { display: none; }
1888
+ }
1889
+ @container (max-width: 450px) {
1890
+ .live-activity-feed .monitor-row .monitor-row-verb { flex-basis: 12px; }
1891
+ .live-activity-feed .monitor-row .monitor-row-verb em { display: none; }
1892
+ .live-activity-feed .monitor-row .monitor-row-plan { display: none; }
1893
+ }
1894
+ @container (max-width: 360px) {
1895
+ .live-activity-feed .monitor-row .monitor-row-meta { display: none; }
1896
+ }
1897
+
1898
+ /* ===== Crew Monitor row density (compact | comfortable) =====
1899
+ Root data attribute set before first paint; auto-picked from viewport width
1900
+ when the operator has expressed no preference. Compact is the single-line row
1901
+ and stays the default on narrow shells.
1902
+ Comfortable is a second layout, not a taller first one: the meta chips stay
1903
+ on line 1 (pushed right), and the primary field reflows to a full-width
1904
+ second line where it gets two clamped lines at 12.5px instead of an ellipsis.
1905
+ The 26px indent aligns it under the badge. The title tooltip stays required
1906
+ in both modes — display `label` is capped at MAX_SEMANTIC_LABEL upstream, so
1907
+ a second line is never a substitute for it. */
1908
+ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row {
1909
+ flex-wrap: wrap;
1910
+ row-gap: 3px;
1911
+ padding: 9px 12px;
1912
+ }
1913
+ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monitor-row-meta {
1801
1914
  margin-left: auto;
1802
1915
  }
1916
+ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monitor-row-primary {
1917
+ order: 10;
1918
+ flex: 1 0 100%;
1919
+ white-space: normal;
1920
+ overflow: hidden;
1921
+ display: -webkit-box;
1922
+ -webkit-line-clamp: 2;
1923
+ -webkit-box-orient: vertical;
1924
+ font-size: 12.5px;
1925
+ line-height: 1.45;
1926
+ padding-left: 26px;
1927
+ }
1928
+ .monitor-density-btn {
1929
+ margin-left: var(--mc-space-sm);
1930
+ }
1803
1931
  .activity-feed-empty {
1804
1932
  padding: var(--mc-space-2xl);
1805
1933
  text-align: center;
@@ -1925,17 +2053,6 @@ body.mc-fullscreen .top-tabs-row {
1925
2053
  vertical-align: middle;
1926
2054
  }
1927
2055
 
1928
- /* ===== Health Investigation Message (legacy; Healthcenter uses .health-detail) ===== */
1929
- .health-message {
1930
- font-size: 11px;
1931
- color: var(--text-secondary);
1932
- padding: 6px 10px;
1933
- margin-top: 4px;
1934
- border-radius: 4px;
1935
- background: var(--bg-secondary);
1936
- border: 1px solid var(--border);
1937
- animation: fadeIn 0.2s ease;
1938
- }
1939
2056
  @keyframes fadeIn {
1940
2057
  from { opacity: 0; transform: translateY(-4px); }
1941
2058
  to { opacity: 1; transform: translateY(0); }
@@ -2518,14 +2635,8 @@ body.mc-fullscreen .top-tabs-row {
2518
2635
  .skill-grid,
2519
2636
  .agent-grid { grid-template-columns: 1fr; }
2520
2637
  .live-activity-feed { max-height: 180px; }
2521
- /* Flex feed-label: allow actor/verb to ellipsis so fixed spans do not hard-clip. */
2522
- .live-activity-feed .monitor-row .feed-seg-actor,
2523
- .live-activity-feed .monitor-row .feed-seg-verb {
2524
- flex-shrink: 1;
2525
- min-width: 0;
2526
- overflow: hidden;
2527
- text-overflow: ellipsis;
2528
- }
2638
+ /* Crew row narrow-width handling moved to the @container ladder on
2639
+ .monitor-cq (design v2): the feed's own width decides, not the window's. */
2529
2640
  }
2530
2641
 
2531
2642
  .overview-stack {
@@ -2758,7 +2869,10 @@ body.mc-fullscreen .top-tabs-row {
2758
2869
  outline: none;
2759
2870
  }
2760
2871
  /* Shared solid status/lifecycle pill: filled semantic surface, no outline,
2761
- slash-command chip geometry. */
2872
+ slash-command chip geometry.
2873
+ Radius (design v2, 2026-08-05): capsule -> --mc-radius-chrome. The capsule
2874
+ read as a foreign shape next to the squared chrome it always sits beside;
2875
+ the fill and the mark carry the state, the outline never did. */
2762
2876
  .lifecycle-pill {
2763
2877
  display: inline-flex;
2764
2878
  align-items: center;
@@ -2767,7 +2881,7 @@ body.mc-fullscreen .top-tabs-row {
2767
2881
  font-weight: 600;
2768
2882
  letter-spacing: 0.02em;
2769
2883
  padding: 2px 6px;
2770
- border-radius: var(--mc-radius-pill);
2884
+ border-radius: var(--mc-radius-chrome);
2771
2885
  border: none;
2772
2886
  color: var(--text-muted);
2773
2887
  background: var(--bg-secondary, transparent);
@@ -2951,7 +3065,7 @@ body.mc-fullscreen .top-tabs-row {
2951
3065
  font-weight: 600;
2952
3066
  letter-spacing: 0.02em;
2953
3067
  padding: 3px var(--mc-space-md);
2954
- border-radius: var(--mc-radius-pill);
3068
+ border-radius: var(--mc-radius-chrome);
2955
3069
  border: none;
2956
3070
  color: var(--text-secondary);
2957
3071
  background: var(--bg-card);
@@ -2976,7 +3090,9 @@ body.mc-fullscreen .top-tabs-row {
2976
3090
  color: var(--green);
2977
3091
  background: var(--green-bg);
2978
3092
  }
2979
- /* Idle: pill radius + hollow mark (shape cue vs squared solid chips) */
3093
+ /* Idle: hollow mark + unfilled surface. The radius stopped carrying this cue
3094
+ when the pills squared off (design v2); the ○ mark is now the whole non-color
3095
+ signal, which is why it may not be dropped. */
2980
3096
  .now-status-idle {
2981
3097
  color: var(--text-muted);
2982
3098
  background: var(--bg-card);
@@ -2991,7 +3107,7 @@ body.mc-fullscreen .top-tabs-row {
2991
3107
  font-weight: var(--mc-chrome-meta-weight);
2992
3108
  letter-spacing: 0.02em;
2993
3109
  padding: 3px var(--mc-space-md);
2994
- border-radius: var(--mc-radius-pill);
3110
+ border-radius: var(--mc-radius-chrome);
2995
3111
  color: var(--blue);
2996
3112
  background: var(--blue-bg);
2997
3113
  text-transform: uppercase;
@@ -3922,6 +4038,61 @@ function setDashboardSkin(skin) {
3922
4038
  return false;
3923
4039
  }
3924
4040
 
4041
+ // ===== Crew Monitor row density (compact | comfortable; local preference) =====
4042
+ const MONITOR_DENSITY_KEY = 'agent-kit:monitor-density';
4043
+ const MONITOR_DENSITIES = ['compact', 'comfortable'];
4044
+ /** Comfortable needs horizontal room for a two-line label to be worth the height. */
4045
+ const MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH = 900;
4046
+
4047
+ function normalizeMonitorDensity(density) {
4048
+ return MONITOR_DENSITIES.includes(density) ? density : null;
4049
+ }
4050
+
4051
+ /** Viewport auto-pick, used only when the operator stored no preference. */
4052
+ function autoMonitorDensity() {
4053
+ return (window.innerWidth || 0) >= MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH
4054
+ ? 'comfortable'
4055
+ : 'compact';
4056
+ }
4057
+
4058
+ function getStoredMonitorDensity() {
4059
+ try {
4060
+ return normalizeMonitorDensity(localStorage.getItem(MONITOR_DENSITY_KEY));
4061
+ } catch (e) {
4062
+ return null;
4063
+ }
4064
+ }
4065
+
4066
+ /** Current mode from the root attribute (set pre-paint), else the auto-pick. */
4067
+ function currentMonitorDensity() {
4068
+ return (
4069
+ normalizeMonitorDensity(document.documentElement.getAttribute('data-monitor-density')) ||
4070
+ getStoredMonitorDensity() ||
4071
+ autoMonitorDensity()
4072
+ );
4073
+ }
4074
+
4075
+ /** Apply density via root data attribute; an explicit choice persists and wins. */
4076
+ function applyMonitorDensity(density, opts) {
4077
+ const next = normalizeMonitorDensity(density) || autoMonitorDensity();
4078
+ document.documentElement.setAttribute('data-monitor-density', next);
4079
+ document.querySelectorAll('[data-monitor-density-toggle]').forEach((el) => {
4080
+ el.setAttribute('aria-pressed', next === 'comfortable' ? 'true' : 'false');
4081
+ el.textContent = next === 'comfortable' ? 'Comfortable' : 'Compact';
4082
+ });
4083
+ if (!opts || opts.persist !== false) {
4084
+ try {
4085
+ localStorage.setItem(MONITOR_DENSITY_KEY, next);
4086
+ } catch (e) { /* private mode / blocked storage */ }
4087
+ }
4088
+ return next;
4089
+ }
4090
+
4091
+ function toggleMonitorDensity() {
4092
+ applyMonitorDensity(currentMonitorDensity() === 'comfortable' ? 'compact' : 'comfortable');
4093
+ return false;
4094
+ }
4095
+
3925
4096
  // ===== More-sections dropdown =====
3926
4097
  function isNavMoreOpen() {
3927
4098
  const menu = document.getElementById('navMoreMenu');
@@ -4190,11 +4361,15 @@ function semanticEventInfo(kind, commitType) {
4190
4361
  // Monitor live kinds: distinct resting color+icon (no kind-tag text, no left rail).
4191
4362
  // run_plan follows .now-status-executing green; delivery uses cyan to avoid green wash.
4192
4363
  // Gloss strings are default software lexicon display masks (kind ids unchanged).
4193
- run_plan: { icon: '\u25b6', tag: 'tick', gloss: 'Tech Lead - live execution', color: 'var(--green)', bg: 'var(--green-bg)' },
4194
- handoff: { icon: '\u23f8', tag: 'handoff', gloss: 'Scrum Master - awaiting gate', color: 'var(--yellow)', bg: 'var(--yellow-bg)' },
4195
- agent_step: { icon: '\u25c9', tag: 'step', gloss: 'Full-Stack Developer - task unit', color: 'var(--purple)', bg: 'var(--purple-bg)' },
4364
+ run_plan: { icon: '\u25b6', tag: 'tick', gloss: 'Project Manager - live execution', color: 'var(--green)', bg: 'var(--green-bg)' },
4365
+ handoff: { icon: '\u23f8', tag: 'handoff', gloss: 'Project Manager - awaiting gate', color: 'var(--yellow)', bg: 'var(--yellow-bg)' },
4366
+ agent_step: { icon: '\u25c9', tag: 'step', gloss: 'Developer - task unit', color: 'var(--purple)', bg: 'var(--purple-bg)' },
4196
4367
  plan_progress: { icon: '\u2691', tag: 'plan', gloss: 'Product Owner - milestone', color: 'var(--orange)', bg: 'var(--orange-bg)' },
4197
- delivery: { icon: '\u2714', tag: 'delivery', gloss: 'DevOps Engineer - shipped unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
4368
+ delivery: { icon: '\u2714', tag: 'delivery', gloss: 'DevOps - merged unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
4369
+ // Live Task-worker lifecycle (running / done / failed) and background
4370
+ // mid-batch review pointers. Both added 2026-08-05; kind ids are additive.
4371
+ subagent: { icon: '\u25cc', tag: 'subagent', gloss: 'Developer - subagent run', color: 'var(--blue)', bg: 'var(--blue-bg)' },
4372
+ plan_review: { icon: '\u2611', tag: 'review', gloss: 'QA - review evidence', color: 'var(--orange)', bg: 'var(--orange-bg)' },
4198
4373
  commit: { icon: '\u25cf', tag: 'commit', gloss: 'commit', color: 'var(--orange)', bg: 'var(--orange-bg)' },
4199
4374
  agent: { icon: '\u25c6', tag: 'agent', gloss: 'agent', color: 'var(--purple)', bg: 'var(--purple-bg)' },
4200
4375
  skill: { icon: '\u2726', tag: 'skill', gloss: 'skill', color: 'var(--orange)', bg: 'var(--orange-bg)' },
@@ -4206,12 +4381,12 @@ function semanticEventInfo(kind, commitType) {
4206
4381
  // Locked BMP map (hotfix 2026-07-27): feat✦ fix⚙ docs✎ chore⚒ pr⑂ ship✈
4207
4382
  if (kind === 'delivery' && commitType) {
4208
4383
  const subtypes = {
4209
- feat: { icon: '\u2726', tag: 'delivery', gloss: 'DevOps Engineer - feat', color: 'var(--purple)', bg: 'var(--purple-bg)' },
4210
- fix: { icon: '\u2699', tag: 'delivery', gloss: 'DevOps Engineer - fix', color: 'var(--red)', bg: 'var(--red-bg)' },
4211
- docs: { icon: '\u270e', tag: 'delivery', gloss: 'DevOps Engineer - docs', color: 'var(--blue)', bg: 'var(--blue-bg)' },
4212
- chore: { icon: '\u2692', tag: 'delivery', gloss: 'DevOps Engineer - chore', color: 'var(--orange)', bg: 'var(--orange-bg)' },
4213
- pr: { icon: '\u2442', tag: 'delivery', gloss: 'DevOps Engineer - PR', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
4214
- ship: { icon: '\u2708', tag: 'delivery', gloss: 'DevOps Engineer - shipped unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
4384
+ feat: { icon: '\u2726', tag: 'delivery', gloss: 'DevOps - feat', color: 'var(--purple)', bg: 'var(--purple-bg)' },
4385
+ fix: { icon: '\u2699', tag: 'delivery', gloss: 'DevOps - fix', color: 'var(--red)', bg: 'var(--red-bg)' },
4386
+ docs: { icon: '\u270e', tag: 'delivery', gloss: 'DevOps - docs', color: 'var(--blue)', bg: 'var(--blue-bg)' },
4387
+ chore: { icon: '\u2692', tag: 'delivery', gloss: 'DevOps - chore', color: 'var(--orange)', bg: 'var(--orange-bg)' },
4388
+ pr: { icon: '\u2442', tag: 'delivery', gloss: 'DevOps - PR', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
4389
+ ship: { icon: '\u2708', tag: 'delivery', gloss: 'DevOps - merged unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
4215
4390
  };
4216
4391
  if (subtypes[commitType]) return subtypes[commitType];
4217
4392
  }
@@ -4220,7 +4395,7 @@ function semanticEventInfo(kind, commitType) {
4220
4395
 
4221
4396
  /** Monitor hero allowlist (curated subset). Inventory kinds stay on the full stream. */
4222
4397
  const MONITOR_ACTIVITY_KINDS = new Set([
4223
- 'run_plan', 'handoff', 'delivery', 'agent_step',
4398
+ 'run_plan', 'handoff', 'delivery', 'agent_step', 'subagent', 'plan_review',
4224
4399
  ]);
4225
4400
  /**
4226
4401
  * Crew Monitor display cap from missionControl.monitorFeedCap
@@ -4415,15 +4590,165 @@ function crewEventTime(ev, info) {
4415
4590
  }
4416
4591
 
4417
4592
  /**
4418
- * Crew feed actor: kit agent id, else Engineering Manager for delivery, else
4419
- * Squad when a plan is present (never the full plan filename), else Platform
4420
- * Engineer. Default software lexicon masks; mirrors briefActivityActor.
4593
+ * Crew feed actor WIRE value, not display. Kit agent id, else Eng for
4594
+ * delivery, else SQ when a plan is present (never the full plan filename),
4595
+ * else Eng. Mirrors briefActivityActor() in dashboard/lib/semantic-model.mjs;
4596
+ * the two must not drift, which is why the masks below live in a separate
4597
+ * layer instead of being folded in here.
4598
+ *
4599
+ * `Eng` still collides between the delivery and system fallbacks on the wire.
4600
+ * The row no longer leaves it ambiguous: crewActorRole() splits it by kind.
4601
+ * Used directly only as the aria-label fallback when a row has no label.
4421
4602
  */
4422
4603
  function crewEventActor(ev) {
4423
4604
  if (ev && ev.agent) return String(ev.agent);
4424
- if (ev && ev.kind === 'delivery') return 'Engineering Manager';
4425
- if (ev && ev.refs && ev.refs.plan) return 'Squad';
4426
- return 'Platform Engineer';
4605
+ if (ev && ev.kind === 'delivery') return 'Eng';
4606
+ if (ev && ev.refs && ev.refs.plan) return 'SQ';
4607
+ return 'Eng';
4608
+ }
4609
+
4610
+ /**
4611
+ * Operator lexicon (design v2, 2026-08-05): wire actor -> display role.
4612
+ * Display only. The wire format is frozen — `labelFull` keeps the raw seg0
4613
+ * (`Eng`, `docs-repo`, …) and the collectors emit exactly what they emitted
4614
+ * before, so nothing here is a backend change.
4615
+ */
4616
+ const CREW_ACTOR_MASK = {
4617
+ 'docs-repo': 'Tech Writer',
4618
+ explore: 'Product Analyst',
4619
+ generalPurpose: 'Analyst',
4620
+ SQ: 'Scrum Master',
4621
+ Dev: 'Developer',
4622
+ QA: 'QA',
4623
+ PO: 'Product Owner',
4624
+ PM: 'Project Manager',
4625
+ };
4626
+
4627
+ /**
4628
+ * The `Eng` split. The lexicon maps two different roles onto the same wire
4629
+ * token, and six consecutive `Eng · merged` rows were the single worst thing in
4630
+ * the shipped feed. `kind` already carries the distinction, so no new wire
4631
+ * field is needed (ACCEPTANCE.md delta B): a delivery row's `Eng` is the
4632
+ * Platform Engineer, every other `Eng` is the Engineering Manager.
4633
+ */
4634
+ function crewActorEngRole(ev) {
4635
+ return ev && ev.kind === 'delivery' ? 'DevOps' : 'Tech Lead';
4636
+ }
4637
+
4638
+ /**
4639
+ * Display role for a Crew row. Resolution order (design contract):
4640
+ * per-event `ev.role` -> mask -> raw token. `ev.role` is optional and nothing
4641
+ * renders from it today; it is honoured so a collector can override the split
4642
+ * later without another dashboard change.
4643
+ * @param {object} ev
4644
+ * @param {string} raw seg0 of the row label (falls back to crewEventActor)
4645
+ */
4646
+ function crewActorRole(ev, raw) {
4647
+ if (ev && ev.role) return String(ev.role);
4648
+ const token = String(raw || crewEventActor(ev) || '').trim();
4649
+ if (token === 'Eng') return crewActorEngRole(ev);
4650
+ return CREW_ACTOR_MASK[token] || token;
4651
+ }
4652
+
4653
+ /**
4654
+ * Badge monogram for a display role. First letter of the first two words,
4655
+ * single word -> first two chars. Two explicit overrides: Developer and DevOps
4656
+ * both start `D`+`e`/`DV`-shaped and would otherwise collide on `DE`.
4657
+ * Distinct from agentInitials(), which monograms a kebab-case agent id for the
4658
+ * Agents card; this one monograms a spaced display role.
4659
+ */
4660
+ function crewActorInitials(role) {
4661
+ const special = { Developer: 'DV', DevOps: 'DO' };
4662
+ if (special[role]) return special[role];
4663
+ const words = String(role || '').trim().split(/\s+/).filter(Boolean);
4664
+ if (words.length === 0) return '?';
4665
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
4666
+ return (words[0][0] + words[1][0]).toUpperCase();
4667
+ }
4668
+
4669
+ /**
4670
+ * Verb state icons. The design specified Feather geometry on a viewBox-24 grid;
4671
+ * these are the same shapes redrawn on the dashboard's own viewBox-16 grid, so
4672
+ * the file keeps ONE icon geometry (spaceIconSvg, nowMetaIconSvg and the header
4673
+ * refresh control are all 16). Importing a second grid for six row marks would
4674
+ * have split the icon system for no legibility gain.
4675
+ *
4676
+ * They are row state marks, not chrome: they render at 12px inside a text
4677
+ * column, so the CSS overrides stroke-width to 1.6 — the 16-grid equivalent of
4678
+ * the design's 2.4-on-24 optical compensation, since 1.5 at 12px renders
4679
+ * lighter than 1.5 at the chrome set's 16px.
4680
+ * Static markup only — the verb is a map key, never concatenated into the SVG.
4681
+ */
4682
+ const CREW_VERB_ICON_PATHS = {
4683
+ play: '<polygon points="3.5 2.5 12.5 8 3.5 13.5"/>',
4684
+ pause: '<line x1="6" y1="3.5" x2="6" y2="12.5"/><line x1="10" y1="3.5" x2="10" y2="12.5"/>',
4685
+ check: '<polyline points="13.5 4 6 11.5 2.5 8"/>',
4686
+ x: '<line x1="12" y1="4" x2="4" y2="12"/><line x1="4" y1="4" x2="12" y2="12"/>',
4687
+ 'git-merge': '<circle cx="12" cy="12" r="2"/><circle cx="4" cy="4" r="2"/><path d="M4 14V6a6 6 0 0 0 6 6"/>',
4688
+ 'circle-slash': '<circle cx="8" cy="8" r="6.5"/><line x1="3.4" y1="3.4" x2="12.6" y2="12.6"/>',
4689
+ info: '<circle cx="8" cy="8" r="6.5"/><line x1="8" y1="11" x2="8" y2="8"/><line x1="8" y1="5" x2="8.01" y2="5"/>',
4690
+ flag: '<path d="M2.7 10s.7-.7 2.7-.7 3.3 1.3 5.3 1.3 2.7-.7 2.7-.7V2s-.7.7-2.7.7-3.3-1.3-5.3-1.3-2.7.7-2.7.7z"/><line x1="2.7" y1="14.7" x2="2.7" y2="10"/>',
4691
+ };
4692
+
4693
+ /** Verb -> state icon. Replaces the 6px state dot (design v2). */
4694
+ const CREW_VERB_ICON = {
4695
+ running: 'play',
4696
+ awaiting: 'pause',
4697
+ done: 'check',
4698
+ failed: 'x',
4699
+ merged: 'git-merge',
4700
+ parked: 'circle-slash',
4701
+ };
4702
+
4703
+ /** Decorative row icon; unknown verbs fall back to `info` rather than nothing. */
4704
+ function crewRowIconSvg(name, className) {
4705
+ const paths = CREW_VERB_ICON_PATHS[name] || CREW_VERB_ICON_PATHS.info;
4706
+ return `<svg class="${className}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
4707
+ }
4708
+
4709
+ /**
4710
+ * Split a Crew row label into the v2 column contract.
4711
+ * Refs (PR, SHA, worker id, plan) leave the flexible text so they can render as
4712
+ * fixed-content chips that hide whole instead of ellipsising. Everything left
4713
+ * over joins the primary field — the row's only wide, only ellipsising column.
4714
+ * Plan detection prefers `refs.plan` over sniffing the rendered string, since
4715
+ * the string is capped at MAX_SEMANTIC_LABEL upstream and may not carry it.
4716
+ */
4717
+ function parseCrewRow(ev) {
4718
+ const segs = String((ev && ev.label) || '').split(' · ');
4719
+ const planRef = ev && ev.refs && ev.refs.plan ? String(ev.refs.plan) : '';
4720
+ const planBase = planRef.replace(/\.plan\.md$/i, '');
4721
+ const out = {
4722
+ actor: segs[0] || '',
4723
+ verb: segs[1] || '',
4724
+ mids: [],
4725
+ pr: null,
4726
+ sha: null,
4727
+ worker: null,
4728
+ plan: planRef || null,
4729
+ };
4730
+ for (let i = 2; i < segs.length; i++) {
4731
+ const seg = segs[i];
4732
+ if (/\.plan\.md$/i.test(seg) || (planRef && (seg === planRef || seg === planBase))) {
4733
+ out.plan = out.plan || seg;
4734
+ continue;
4735
+ }
4736
+ if (/^PR #\d+/.test(seg)) {
4737
+ out.pr = seg.replace(/^PR /, '');
4738
+ continue;
4739
+ }
4740
+ if (/^[0-9a-f]{7,8}$/.test(seg)) {
4741
+ // A subagent's trailing hex is a worker id, not a commit — same shape,
4742
+ // different meaning, and only the SHA chip is droppable at ≤560px.
4743
+ if (ev && ev.kind === 'subagent') out.worker = seg;
4744
+ else out.sha = seg;
4745
+ continue;
4746
+ }
4747
+ out.mids.push(seg);
4748
+ }
4749
+ // A row with no mid segments still needs something in its wide column.
4750
+ out.primary = out.mids.join(' · ') || planBase || out.verb;
4751
+ return out;
4427
4752
  }
4428
4753
 
4429
4754
  /** Avatar-style monogram: first letters of the first two kebab/space parts, else first two chars. */
@@ -5164,10 +5489,15 @@ const CONFIG_PERSONA_MODES = [
5164
5489
  ];
5165
5490
 
5166
5491
  function personaOptionHtml(selected) {
5167
- return CONFIG_PERSONA_OPTIONS.map((id) => {
5168
- const sel = id === selected ? ' selected' : '';
5169
- return `<option value="${escapeAttr(id)}"${sel}>${escapeHtml(id)}</option>`;
5170
- }).join('');
5492
+ const inheritSel = selected === '' ? ' selected' : '';
5493
+ const inherit = `<option value=""${inheritSel}>Inherit default</option>`;
5494
+ return (
5495
+ inherit +
5496
+ CONFIG_PERSONA_OPTIONS.map((id) => {
5497
+ const sel = id === selected ? ' selected' : '';
5498
+ return `<option value="${escapeAttr(id)}"${sel}>${escapeHtml(id)}</option>`;
5499
+ }).join('')
5500
+ );
5171
5501
  }
5172
5502
 
5173
5503
  function renderConfigSection(d) {
@@ -5201,7 +5531,7 @@ function renderConfigSection(d) {
5201
5531
  <div class="config-row">
5202
5532
  <label for="config-persona-${escapeAttr(mode.id)}">${escapeHtml(mode.label)} persona</label>
5203
5533
  <select id="config-persona-${escapeAttr(mode.id)}" name="personaMode:${escapeAttr(mode.id)}" data-focus-key="config-persona-${escapeAttr(mode.id)}">
5204
- ${personaOptionHtml(current || (ap.default || 'autopilot'))}
5534
+ ${personaOptionHtml(current)}
5205
5535
  </select>
5206
5536
  </div>`;
5207
5537
  }).join('');
@@ -5276,9 +5606,31 @@ function renderConfigSection(d) {
5276
5606
  <div class="config-row">
5277
5607
  <label for="config-epr-backend">Backend</label>
5278
5608
  <select id="config-epr-backend" name="eprBackend" data-focus-key="config-epr-backend">
5609
+ <option value="auto"${epr.backend === 'auto' ? ' selected' : ''}>auto</option>
5279
5610
  <option value="claude"${epr.backend === 'claude' || !epr.backend ? ' selected' : ''}>claude</option>
5611
+ <option value="cursor"${epr.backend === 'cursor' ? ' selected' : ''}>cursor</option>
5280
5612
  </select>
5281
- <span class="config-hint">claude is the only backend today.</span>
5613
+ <span class="config-hint">auto uses Claude when usable, else Cursor Agent. Pin claude or cursor to force one backend.</span>
5614
+ </div>
5615
+ <div class="config-row">
5616
+ <label for="config-epr-reviewerModel">Reviewer model</label>
5617
+ <input type="text" id="config-epr-reviewerModel" name="eprReviewerModel" value="${escapeAttr(epr.reviewerModel || 'haiku')}" maxlength="64" data-focus-key="config-epr-reviewerModel" />
5618
+ <span class="config-hint">Claude default haiku. Must differ from the implementer stamp (Auto/Auto refused).</span>
5619
+ </div>
5620
+ <div class="config-row">
5621
+ <label for="config-epr-advisorModel">Advisor model</label>
5622
+ <input type="text" id="config-epr-advisorModel" name="eprAdvisorModel" value="${escapeAttr(epr.advisorModel || 'opus')}" maxlength="64" data-focus-key="config-epr-advisorModel" />
5623
+ <span class="config-hint">Opus only when the monitor marks high severity or uncertainty.</span>
5624
+ </div>
5625
+ <div class="config-row">
5626
+ <label for="config-epr-waitSlice">Wait slice (seconds)</label>
5627
+ <input type="number" id="config-epr-waitSlice" name="eprWaitSlice" min="1" max="3600" step="1" value="${escapeAttr(String(epr.waitSliceSeconds || 90))}" data-focus-key="config-epr-waitSlice" />
5628
+ <span class="config-hint">Chat AwaitShell budget per session. Default 90.</span>
5629
+ </div>
5630
+ <div class="config-row">
5631
+ <label for="config-epr-waitTimeout">Wait timeout (seconds)</label>
5632
+ <input type="number" id="config-epr-waitTimeout" name="eprWaitTimeout" min="1" max="86400" step="1" value="${escapeAttr(String(epr.waitTimeoutSeconds || 900))}" data-focus-key="config-epr-waitTimeout" />
5633
+ <span class="config-hint">Total remaining budget across slices and CI. Default 900.</span>
5282
5634
  </div>
5283
5635
  <div class="config-row">
5284
5636
  <label for="config-epr-mode">Arming mode</label>
@@ -5350,6 +5702,18 @@ function collectMissionConfigPayload() {
5350
5702
  offerOnExhausted: !!document.getElementById('config-epr-offer')?.checked,
5351
5703
  autoRemediate: !!document.getElementById('config-epr-auto')?.checked,
5352
5704
  backend: document.getElementById('config-epr-backend')?.value || 'claude',
5705
+ reviewerModel: (document.getElementById('config-epr-reviewerModel')?.value || 'haiku').trim(),
5706
+ advisorModel: (document.getElementById('config-epr-advisorModel')?.value || 'opus').trim(),
5707
+ waitSliceSeconds: (() => {
5708
+ const raw = document.getElementById('config-epr-waitSlice')?.value;
5709
+ const n = Number.parseInt(String(raw || '90'), 10);
5710
+ return Number.isFinite(n) && n >= 1 ? n : 90;
5711
+ })(),
5712
+ waitTimeoutSeconds: (() => {
5713
+ const raw = document.getElementById('config-epr-waitTimeout')?.value;
5714
+ const n = Number.parseInt(String(raw || '900'), 10);
5715
+ return Number.isFinite(n) && n >= 1 ? n : 900;
5716
+ })(),
5353
5717
  mode: document.getElementById('config-epr-mode')?.value || 'paste',
5354
5718
  midBatchAudits: !!document.getElementById('config-epr-midBatch')?.checked,
5355
5719
  preflight: document.getElementById('config-epr-preflight')?.value || 'off',
@@ -5361,7 +5725,9 @@ function collectMissionConfigPayload() {
5361
5725
  };
5362
5726
  for (const mode of CONFIG_PERSONA_MODES) {
5363
5727
  const el = document.getElementById(`config-persona-${mode.id}`);
5364
- if (el && el.value) payload.agentPersona.modes[mode.id] = el.value;
5728
+ if (!el) continue;
5729
+ // Empty = Inherit default: send null so merge deletes a stored override.
5730
+ payload.agentPersona.modes[mode.id] = el.value ? el.value : null;
5365
5731
  }
5366
5732
  return payload;
5367
5733
  }
@@ -5452,7 +5818,7 @@ const HEALTH_CHECK_META = {
5452
5818
  handoff: {
5453
5819
  okDetail: 'HANDOFF.md is present and parseable (idle Plan is still healthy).',
5454
5820
  failDetail: 'HANDOFF.md is missing or not parseable.',
5455
- autofix: { text: '/continue-plan', subject: '/continue-plan', destination: 'chatInput', label: 'Autofix' },
5821
+ autofix: { text: '/handoff', subject: '/handoff', destination: 'chatInput', label: 'Autofix' },
5456
5822
  },
5457
5823
  agents: {
5458
5824
  okDetail: 'Agents are optional in L0; empty .cursor/agents/ is healthy. Cataloged agents (if any) are listed in Agents.',
@@ -5468,12 +5834,13 @@ const HEALTH_CHECK_META = {
5468
5834
  memory: {
5469
5835
  okDetail: 'Memory has recorded errors and/or decisions.',
5470
5836
  failDetail: 'Memory index has no errors or decisions yet (optional until first WRITE).',
5471
- autofix: { text: '/agent-kit-onboard', subject: '/agent-kit-onboard', destination: 'chatInput', label: 'Autofix' },
5837
+ autofix: { text: '.cursor/memory/', subject: 'path .cursor/memory/', destination: 'filePicker', label: 'Copy path' },
5472
5838
  },
5473
5839
  git: {
5474
5840
  okDetail: 'Git branch signal is available.',
5475
5841
  failDetail: 'Git repository signal missing (no branch).',
5476
- autofix: { text: 'git status', subject: 'git status', destination: 'terminal', label: 'Autofix' },
5842
+ // Covers not-a-repo and git-init-but-zero-commits (rev-parse HEAD fails until first commit).
5843
+ autofix: { text: 'git init && git commit --allow-empty -m "init"', subject: 'git init && git commit --allow-empty -m "init"', destination: 'terminal', label: 'Autofix' },
5477
5844
  },
5478
5845
  config: {
5479
5846
  okDetail: 'Workspace config is loaded.',
@@ -5491,23 +5858,28 @@ const HEALTH_VITAL_GROUPS = [
5491
5858
  ];
5492
5859
 
5493
5860
  function healthCheckSeverity(check, aggregateStatus) {
5861
+ // Per-check levels are ok | warning | degraded only. Aggregate snapshot/serve
5862
+ // `error` is empty-checks (presence + empty state); producers never emit
5863
+ // status:error with checks present, so do not invent a per-check error sev.
5494
5864
  if (check && check.ok) return 'ok';
5495
5865
  if (aggregateStatus === 'degraded') return 'degraded';
5496
- if (aggregateStatus === 'error') return 'error';
5497
5866
  return 'warning';
5498
5867
  }
5499
5868
 
5500
- /* Single severity chrome mapping (ok | warning | degraded | error): tone drives
5501
- the dot class, label drives text/aria, token names the [data-sev] CSS color. */
5869
+ /* Severity chrome: per-check uses ok | warning | degraded; aggregate `error`
5870
+ remains for snapshot/serve presence only (empty checks). Tone drives the dot
5871
+ class; label drives text/aria. Per-check [data-sev] CSS matches the severity
5872
+ key written on the element, not a chrome field. Aggregate error has no
5873
+ per-check CSS (producers never emit error+checks). */
5502
5874
  const HEALTH_SEVERITY_CHROME = {
5503
- ok: { tone: 'green', label: 'ok', token: 'green' },
5504
- warning: { tone: 'yellow', label: 'warn', token: 'yellow' },
5505
- degraded: { tone: 'orange', label: 'degraded', token: 'orange' },
5506
- error: { tone: 'red', label: 'error', token: 'red' },
5875
+ ok: { tone: 'green', label: 'ok' },
5876
+ warning: { tone: 'yellow', label: 'warn' },
5877
+ degraded: { tone: 'orange', label: 'degraded' },
5878
+ error: { tone: 'red', label: 'error' },
5507
5879
  };
5508
5880
 
5509
5881
  function healthSeverityChrome(sev) {
5510
- return HEALTH_SEVERITY_CHROME[sev] || { tone: 'gray', label: sev || 'unknown', token: 'text-muted' };
5882
+ return HEALTH_SEVERITY_CHROME[sev] || { tone: 'gray', label: sev || 'unknown' };
5511
5883
  }
5512
5884
 
5513
5885
  function healthSeverityLabel(sev) {
@@ -5526,13 +5898,6 @@ function toggleHealthCheck(checkId) {
5526
5898
  if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
5527
5899
  }
5528
5900
 
5529
- function healthCheckKeydown(event, checkId) {
5530
- if (event.key === 'Enter' || event.key === ' ') {
5531
- event.preventDefault();
5532
- toggleHealthCheck(checkId);
5533
- }
5534
- }
5535
-
5536
5901
  // ===== Memory tab: interactive error rows (expand + copy fix prompt) =====
5537
5902
  function toggleMemoryError(idx) {
5538
5903
  const card = document.getElementById('memory-error-' + idx);
@@ -5584,19 +5949,6 @@ function renderMemoryErrorRow(entry, idx) {
5584
5949
  `;
5585
5950
  }
5586
5951
 
5587
- /** @deprecated Prefer toggleHealthCheck; kept for any leftover callers. */
5588
- function showHealthInfo(el, ok) {
5589
- const parent = el.parentElement;
5590
- const existing = parent.querySelector('.health-message');
5591
- if (existing) { existing.remove(); return; }
5592
- const msg = document.createElement('div');
5593
- msg.className = 'health-message';
5594
- msg.textContent = ok
5595
- ? 'All good: no action needed.'
5596
- : 'Check may indicate an issue. Review the relevant logs or configuration.';
5597
- parent.appendChild(msg);
5598
- }
5599
-
5600
5952
  // ===== Get connection uptime (Phase 2) =====
5601
5953
  function getConnectionUptime() {
5602
5954
  if (sseMode !== 'live') return '';
@@ -7438,8 +7790,9 @@ function renderUnsafe() {
7438
7790
  <div class="live-activity-header">
7439
7791
  <span class="card-title">${spaceIconSvg('monitor')}Crew Monitor</span>
7440
7792
  <span class="section-subtitle" style="margin-left:auto">${feedEvents.length} shown</span>
7793
+ <button type="button" class="git-action-btn monitor-density-btn" data-monitor-density-toggle data-focus-key="monitor-density-toggle" aria-pressed="${currentMonitorDensity() === 'comfortable' ? 'true' : 'false'}" title="Toggle Crew Monitor row density (compact single-line rows vs comfortable two-line rows)" onclick="toggleMonitorDensity()">${currentMonitorDensity() === 'comfortable' ? 'Comfortable' : 'Compact'}</button>
7441
7794
  </div>
7442
- <div class="live-activity-feed">
7795
+ <div class="live-activity-feed monitor-cq">
7443
7796
  ${feedEvents.length === 0
7444
7797
  ? renderEmptyStateCta({
7445
7798
  headline: 'Listening',
@@ -7453,42 +7806,56 @@ function renderUnsafe() {
7453
7806
  const info = semanticEventInfo(ev.kind, ev.refs?.commitType);
7454
7807
  const gloss = info.gloss || info.tag || ev.kind || 'event';
7455
7808
  const staggerIdx = Math.min(idx, 5);
7456
- const actor = crewEventActor(ev);
7457
- const initials = agentInitials(actor);
7809
+ const row = parseCrewRow(ev);
7810
+ const role = crewActorRole(ev, row.actor);
7811
+ const rawActor = row.actor || crewEventActor(ev);
7458
7812
  const actionAttrs = activityTargetAttributes(ev, 'monitor-activity', { kindGloss: gloss });
7459
7813
  const rowA11y = actionAttrs
7460
7814
  ? actionAttrs
7461
- : ` aria-label="${escapeAttr(`${gloss}: ${ev.label || ''}`)}"`;
7462
- // Structured label spans: actor + verb stay fixed; later segments
7463
- // shrink, and the low-signal plan filename ellipsises first.
7464
- // Prefer refs.plan over sniffing the rendered string (truncation-safe).
7465
- const planRef = ev.refs && ev.refs.plan ? String(ev.refs.plan) : '';
7466
- const planBase = planRef.replace(/\.plan\.md$/i, '');
7467
- const feedSegs = String(ev.label || '').split(' · ');
7468
- const feedLabelHtml = feedSegs
7469
- .map((seg, i) => {
7470
- const isPlanSeg = Boolean(planRef) && (
7471
- seg === planRef ||
7472
- seg === planBase ||
7473
- /\.plan\.md/i.test(seg)
7474
- );
7475
- const cls = i === 0
7476
- ? 'feed-seg feed-seg-actor'
7477
- : i === 1
7478
- ? 'feed-seg feed-seg-verb'
7479
- : isPlanSeg
7480
- ? 'feed-seg feed-seg-plan'
7481
- : 'feed-seg feed-seg-mid';
7482
- return `<span class="${cls}">${escapeHtml(seg)}</span>`;
7483
- })
7484
- .join('<span class="feed-sep" aria-hidden="true"> · </span>');
7485
- const feedTitle = ev.labelFull || ev.label || '';
7815
+ : ` aria-label="${escapeAttr(`${gloss}: ${ev.label || role}`)}"`;
7816
+ // Row order (design v2, 2026-08-05): team badge, actor, verb,
7817
+ // primary, ref chips, time. The kind glyph is gone — its colour
7818
+ // moved onto the badge and its meaning onto the badge tooltip, so
7819
+ // nothing composes a glyph into the text and a flat single-segment
7820
+ // label cannot misplace one. `info.bg` tints the badge; that is the
7821
+ // only place a Crew row carries it.
7822
+ const badgeHtml = `<span class="monitor-row-badge" style="background:${info.bg};color:${info.color}" title="${escapeAttr(gloss)}" aria-hidden="true">${escapeHtml(crewActorInitials(role))}</span>`;
7823
+ // Actor tooltip keeps the wire token next to the mask, so an
7824
+ // operator reading `Tech Lead` can still see it came from `Eng`.
7825
+ const actorHtml = `<span class="monitor-row-actor" title="${escapeAttr(`${role} (${rawActor})`)}">${escapeHtml(role)}</span>`;
7826
+ const verbIcon = crewRowIconSvg(CREW_VERB_ICON[row.verb] || 'info', 'monitor-row-verb-icon');
7827
+ const verbHtml = row.verb
7828
+ ? `<span class="monitor-row-verb v-${escapeAttr(row.verb)}" title="${escapeAttr(row.verb)}">${verbIcon}<em>${escapeHtml(row.verb)}</em></span>`
7829
+ : '';
7830
+ // Ref chips: fixed content, dropped whole by the @container
7831
+ // ladder. Never ellipsised — half a SHA is worse than no SHA.
7832
+ const refsHtml = [
7833
+ row.pr ? `<span class="monitor-row-ref ref-pr">${escapeHtml(row.pr)}</span>` : '',
7834
+ row.sha ? `<span class="monitor-row-ref ref-sha">${escapeHtml(row.sha)}</span>` : '',
7835
+ row.worker ? `<span class="monitor-row-ref ref-worker" title="worker id">${escapeHtml(row.worker)}</span>` : '',
7836
+ row.plan
7837
+ ? `<span class="monitor-row-plan" title="${escapeAttr(row.plan)}">${crewRowIconSvg('flag', 'monitor-row-plan-flag')}<span class="monitor-row-plan-name">${escapeHtml(row.plan.replace(/\.plan\.md$/i, ''))}</span></span>`
7838
+ : '',
7839
+ ].join('');
7840
+ // labelFull (+ sourcePath) must be reachable in every layout:
7841
+ // display `label` is capped at MAX_SEMANTIC_LABEL upstream, so
7842
+ // no layout is a substitute for it. It rides on .monitor-row-
7843
+ // primary rather than the row, because an actionable row's own
7844
+ // title is already spoken for by activityTargetAttributes
7845
+ // (the copy-path/copy-sha affordance) and a second title
7846
+ // attribute on the same element is silently dropped. Primary is
7847
+ // also the only field that ellipsises, so the full text sits on
7848
+ // exactly the element whose text is cut.
7849
+ const feedTitle = (ev.labelFull || ev.label || '') + (ev.sourcePath ? `\n${ev.sourcePath}` : '');
7850
+ const rowTitle = actionAttrs ? '' : ` title="${escapeAttr(feedTitle)}"`;
7486
7851
  return `
7487
- <div class="monitor-row stagger-fade stagger-${staggerIdx}"${rowA11y}>
7488
- <span class="monitor-row-avatar" title="${escapeAttr(actor)}" aria-hidden="true">${escapeHtml(initials)}</span>
7489
- <span class="monitor-row-chip" style="background:${info.bg};color:${info.color}" title="${escapeAttr(gloss)}" aria-hidden="true"><span class="monitor-row-icon">${info.icon}</span></span>
7490
- <span class="feed-label" title="${escapeAttr(feedTitle)}">${feedLabelHtml}</span>
7491
- <span class="feed-time">${crewEventTime(ev, info)}</span>
7852
+ <div class="monitor-row stagger-fade stagger-${staggerIdx}"${rowA11y}${rowTitle}>
7853
+ ${badgeHtml}
7854
+ ${actorHtml}
7855
+ ${verbHtml}
7856
+ <span class="monitor-row-primary" title="${escapeAttr(feedTitle)}">${escapeHtml(row.primary)}</span>
7857
+ <span class="monitor-row-meta">${refsHtml}</span>
7858
+ <span class="monitor-row-time">${crewEventTime(ev, info)}</span>
7492
7859
  </div>`;
7493
7860
  }).join('')}
7494
7861
  </div>
@@ -7698,7 +8065,7 @@ function renderUnsafe() {
7698
8065
  : `${fixBtn}${autofixBtn}`;
7699
8066
  return `
7700
8067
  <div class="health-card" role="listitem" id="health-card-${escapeAttr(id)}" data-check-id="${escapeAttr(id)}" data-severity="${escapeAttr(sev)}">
7701
- <button type="button" class="health-item" aria-expanded="false" aria-controls="health-detail-${escapeAttr(id)}" aria-label="${escapeAttr(c.label)}: ${escapeAttr(healthSeverityLabel(sev))}" onclick="toggleHealthCheck('${escapeJsString(id)}')" onkeydown="healthCheckKeydown(event, '${escapeJsString(id)}')">
8068
+ <button type="button" class="health-item" aria-expanded="false" aria-controls="health-detail-${escapeAttr(id)}" aria-label="${escapeAttr(c.label)}: ${escapeAttr(healthSeverityLabel(sev))}" onclick="toggleHealthCheck('${escapeJsString(id)}')">
7702
8069
  <span class="dot ${healthDotClass(sev)}"></span>
7703
8070
  <span class="health-item-label">${escapeHtml(c.label)}</span>
7704
8071
  <span class="health-item-sev" data-sev="${escapeAttr(sev)}">${escapeHtml(healthSeverityLabel(sev))}</span>