instar 1.3.787 → 1.3.789

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.
@@ -55,10 +55,27 @@
55
55
  position: relative;
56
56
  }
57
57
 
58
- /* Hamburger togglevisible only on mobile, sits in the header row */
58
+ /* Navigation menu button grouped-nav trigger at ALL widths (F2, dashboard-ux-standard).
59
+ Reachability floor: 25 tabs can't fit a flat bar, so a single grouped menu opens from here. */
59
60
  .nav-toggle {
60
- display: none;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 8px;
64
+ margin-left: 16px;
65
+ padding: 8px 14px;
66
+ min-height: 38px;
67
+ background: var(--bg);
68
+ border: 1px solid var(--border);
69
+ border-radius: 6px;
70
+ color: var(--text);
71
+ font-size: 14px;
72
+ font-weight: 500;
73
+ cursor: pointer;
74
+ flex-shrink: 0;
61
75
  }
76
+ .nav-toggle:hover { background: var(--bg-hover); }
77
+ .nav-toggle[aria-expanded="true"] { background: var(--bg-active); border-color: var(--accent); }
78
+ #navToggleLabel { font-weight: 600; color: var(--text-bright); }
62
79
 
63
80
  .header-left {
64
81
  display: flex;
@@ -542,6 +559,28 @@
542
559
  background: var(--bg);
543
560
  }
544
561
 
562
+ /* F3 — Dashboard UX Standard: every tab's plain-language purpose line.
563
+ One muted, readable sentence directly under the tab header telling a
564
+ non-expert what the tab is for and what they can do here. Enforced by
565
+ tests/unit/dashboard-tab-purpose.test.ts.
566
+
567
+ F7 — CANONICAL purpose-line class. `.tab-purpose` is the shared vocabulary
568
+ every NEW tab should use for its purpose line. Three legacy variants still
569
+ exist and carry their own deliberately-distinct sizing — `.ph-intro`
570
+ (Process-Health design-system intro, larger), `.features-subtitle`, and
571
+ `.dropzone-subtitle` — all recognized by the F3 floor test. Per the spec
572
+ (docs/specs/dashboard-ux-standard.md, FD-3) F7 is soft-first: consolidating
573
+ those three into `.tab-purpose` is a tracked migration (it changes their
574
+ visual size, so it is done deliberately, not big-bang). Until then: reach
575
+ for `.tab-purpose`; do not add a fifth variant or an inline-styled line. */
576
+ .tab-purpose {
577
+ font-size: 13px;
578
+ color: var(--text-dim);
579
+ line-height: 1.5;
580
+ margin: 0 0 4px 0;
581
+ max-width: 900px;
582
+ }
583
+
545
584
  .terminal-header {
546
585
  display: flex;
547
586
  align-items: center;
@@ -767,41 +806,69 @@
767
806
  ::-webkit-scrollbar-thumb:hover { background: #444; }
768
807
 
769
808
  /* ── Tab bar ──────────────────────────────────────────── */
809
+ /* Grouped navigation menu (F2, dashboard-ux-standard) — one dropdown at ALL widths,
810
+ opened by .nav-toggle. Hidden until .app.nav-open. Replaces the old horizontal bar
811
+ that clipped 17 of 25 tabs with no reachable affordance. */
770
812
  .tab-bar {
771
- display: flex;
772
- gap: 0;
773
- margin-left: 24px;
774
- overflow-x: auto;
775
- -webkit-overflow-scrolling: touch;
776
- scrollbar-width: none; /* hide scrollbar on Firefox */
813
+ position: absolute;
814
+ top: 100%;
815
+ left: 16px;
816
+ width: 320px;
817
+ max-width: calc(100vw - 32px);
818
+ display: none;
819
+ flex-direction: column;
820
+ gap: 2px;
821
+ padding: 8px;
822
+ background: var(--bg-panel);
823
+ border: 1px solid var(--border);
824
+ border-radius: 0 0 10px 10px;
825
+ box-shadow: 0 8px 28px rgba(0,0,0,0.45);
826
+ z-index: 100;
827
+ max-height: calc(100vh - 64px);
828
+ overflow-y: auto;
777
829
  }
778
- .tab-bar::-webkit-scrollbar { display: none; }
830
+ .app.nav-open .tab-bar { display: flex; }
779
831
 
780
- .tab-bar .tab {
781
- padding: 6px 16px;
782
- border: 1px solid var(--border);
783
- border-radius: 6px 6px 0 0;
784
- background: var(--bg);
832
+ /* Group headers inside the menu (grouped sections). */
833
+ .tab-group { display: flex; flex-direction: column; gap: 2px; }
834
+ .tab-group + .tab-group { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }
835
+ .tab-group-label {
836
+ font-size: 11px;
837
+ font-weight: 700;
838
+ letter-spacing: 0.06em;
839
+ text-transform: uppercase;
785
840
  color: var(--text-dim);
786
- font-size: 13px;
841
+ padding: 4px 10px 2px;
842
+ }
843
+
844
+ .tab-bar .tab {
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: space-between;
848
+ gap: 8px;
849
+ width: 100%;
850
+ text-align: left;
851
+ padding: 9px 12px;
852
+ min-height: 40px;
853
+ border: 1px solid transparent;
854
+ border-radius: 6px;
855
+ background: transparent;
856
+ color: var(--text);
857
+ font-size: 14px;
787
858
  font-weight: 500;
788
859
  cursor: pointer;
789
- border-bottom: none;
790
- margin-bottom: -1px;
791
860
  transition: color 0.15s, background 0.15s;
792
- white-space: nowrap;
793
- flex-shrink: 0;
794
861
  }
795
862
 
796
863
  .tab-bar .tab:hover {
797
- color: var(--text);
864
+ color: var(--text-bright);
798
865
  background: var(--bg-hover);
799
866
  }
800
867
 
801
868
  .tab-bar .tab.active {
802
- background: var(--bg-panel);
869
+ background: var(--bg-active);
803
870
  color: var(--accent);
804
- border-color: var(--border);
871
+ border-color: var(--accent);
805
872
  }
806
873
 
807
874
  .tab-bar .tab-count {
@@ -1008,6 +1075,7 @@
1008
1075
  .files-container {
1009
1076
  display: flex;
1010
1077
  grid-column: 1 / -1;
1078
+ min-width: 0; /* F4 — a grid child must be able to shrink below content width, else it blows out the row and the body scrolls sideways */
1011
1079
  overflow: hidden;
1012
1080
  background: var(--bg);
1013
1081
  }
@@ -1819,6 +1887,7 @@
1819
1887
  .jobs-container {
1820
1888
  display: flex;
1821
1889
  grid-column: 1 / -1;
1890
+ min-width: 0; /* F4 — grid child must shrink below content width (no body h-scroll) */
1822
1891
  overflow: hidden;
1823
1892
  background: var(--bg);
1824
1893
  }
@@ -2193,6 +2262,7 @@
2193
2262
  /* ── Features Tab (was Discovery) ──────────────────────────── */
2194
2263
  .features-container {
2195
2264
  grid-column: 1 / -1;
2265
+ min-width: 0; /* F4 — grid child must shrink below content width (no body h-scroll) */
2196
2266
  overflow-y: auto;
2197
2267
  background: var(--bg);
2198
2268
  padding: 20px;
@@ -2284,7 +2354,7 @@
2284
2354
  .feat-reversibility { font-size: 12px; color: var(--text-dim); line-height: 1.5; padding: 12px 14px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; }
2285
2355
 
2286
2356
  /* ── Systems Tab ──────────────────────────────────────────── */
2287
- .systems-container { grid-column: 1 / -1; overflow-y: auto; background: var(--bg); padding: 20px; }
2357
+ .systems-container { grid-column: 1 / -1; min-width: 0; overflow-y: auto; background: var(--bg); padding: 20px; }
2288
2358
  .systems-main { max-width: 960px; margin: 0 auto; }
2289
2359
  .systems-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
2290
2360
  .systems-header h2 { font-size: 16px; font-weight: 600; color: var(--text-bright); }
@@ -2750,32 +2820,47 @@
2750
2820
  <img class="logo" src="/dashboard/logo.png" alt="Instar">
2751
2821
  <h1>Instar Dashboard</h1>
2752
2822
  <button class="nav-toggle" id="navToggle" aria-label="Open navigation menu" aria-expanded="false" onclick="toggleNavMenu()">&#9776; <span id="navToggleLabel">Sessions</span></button>
2753
- <nav class="tab-bar" id="tabBar">
2754
- <button class="tab active" data-tab="sessions" onclick="switchTab('sessions')">Sessions <span class="tab-count" id="tabSessionCount">0</span></button>
2755
- <button class="tab" data-tab="files" onclick="switchTab('files')">Files</button>
2756
- <button class="tab" data-tab="dropzone" onclick="switchTab('dropzone')">Send Content</button>
2757
- <button class="tab" data-tab="secrets" onclick="switchTab('secrets')">Secrets <span class="tab-count" id="tabSecretCount">0</span></button>
2758
- <button class="tab" data-tab="jobs" onclick="switchTab('jobs')">Jobs <span class="tab-count" id="tabJobCount">0</span></button>
2759
- <button class="tab" data-tab="features" onclick="switchTab('features')">Features</button>
2760
- <button class="tab" data-tab="systems" onclick="switchTab('systems')">Health</button>
2761
- <button class="tab" data-tab="integrated-being" onclick="switchTab('integrated-being')">Integrated-Being</button>
2762
- <button class="tab" data-tab="pr-pipeline" onclick="switchTab('pr-pipeline')">PR Pipeline</button>
2763
- <button class="tab" data-tab="projects" onclick="switchTab('projects')">Projects <span class="tab-count" id="tabProjectCount">0</span></button>
2764
- <button class="tab" data-tab="initiatives" onclick="switchTab('initiatives')">Initiatives <span class="tab-count" id="tabInitiativeCount">0</span></button>
2765
- <button class="tab" data-tab="commitments" onclick="switchTab('commitments')">Commitments <span class="tab-count" id="tabCommitmentCount">0</span></button>
2766
- <button class="tab" data-tab="tokens" onclick="switchTab('tokens')">Tokens</button>
2767
- <button class="tab" data-tab="resources" onclick="switchTab('resources')">Resource Usage</button>
2768
- <button class="tab" data-tab="llm-activity" onclick="switchTab('llm-activity')">LLM Activity</button>
2769
- <button class="tab" data-tab="routing-map" onclick="switchTab('routing-map')">Routing Map</button>
2770
- <button class="tab" data-tab="spend" onclick="switchTab('spend')">Spend</button>
2771
- <button class="tab" data-tab="threadline" onclick="switchTab('threadline')">Threadline</button>
2772
- <button class="tab" data-tab="evidence" onclick="switchTab('evidence')">Evidence</button>
2773
- <button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
2774
- <button class="tab" data-tab="subscriptions" onclick="switchTab('subscriptions')">Subscriptions</button>
2775
- <button class="tab" data-tab="preferences-learning" onclick="switchTab('preferences-learning')">Preferences</button>
2776
- <button class="tab" data-tab="machines" onclick="switchTab('machines')">Machines</button>
2777
- <button class="tab" data-tab="mandates" onclick="switchTab('mandates')">Mandates</button>
2778
- <button class="tab" data-tab="blockers" onclick="switchTab('blockers')">Blockers</button>
2823
+ <nav class="tab-bar" id="tabBar" aria-label="Dashboard sections">
2824
+ <div class="tab-group" role="group" aria-label="Runtime">
2825
+ <div class="tab-group-label">Runtime</div>
2826
+ <button class="tab active" data-tab="sessions" onclick="switchTab('sessions')">Sessions <span class="tab-count" id="tabSessionCount">0</span></button>
2827
+ <button class="tab" data-tab="machines" onclick="switchTab('machines')">Machines</button>
2828
+ <button class="tab" data-tab="jobs" onclick="switchTab('jobs')">Jobs <span class="tab-count" id="tabJobCount">0</span></button>
2829
+ <button class="tab" data-tab="systems" onclick="switchTab('systems')">Health</button>
2830
+ </div>
2831
+ <div class="tab-group" role="group" aria-label="Work & Delivery">
2832
+ <div class="tab-group-label">Work &amp; Delivery</div>
2833
+ <button class="tab" data-tab="pr-pipeline" onclick="switchTab('pr-pipeline')">PR Pipeline</button>
2834
+ <button class="tab" data-tab="projects" onclick="switchTab('projects')">Projects <span class="tab-count" id="tabProjectCount">0</span></button>
2835
+ <button class="tab" data-tab="initiatives" onclick="switchTab('initiatives')">Initiatives <span class="tab-count" id="tabInitiativeCount">0</span></button>
2836
+ <button class="tab" data-tab="commitments" onclick="switchTab('commitments')">Commitments <span class="tab-count" id="tabCommitmentCount">0</span></button>
2837
+ <button class="tab" data-tab="blockers" onclick="switchTab('blockers')">Blockers</button>
2838
+ <button class="tab" data-tab="evidence" onclick="switchTab('evidence')">Evidence</button>
2839
+ </div>
2840
+ <div class="tab-group" role="group" aria-label="Cost & Routing">
2841
+ <div class="tab-group-label">Cost &amp; Routing</div>
2842
+ <button class="tab" data-tab="spend" onclick="switchTab('spend')">Spend</button>
2843
+ <button class="tab" data-tab="subscriptions" onclick="switchTab('subscriptions')">Subscriptions</button>
2844
+ <button class="tab" data-tab="routing-map" onclick="switchTab('routing-map')">Routing Map</button>
2845
+ <button class="tab" data-tab="llm-activity" onclick="switchTab('llm-activity')">LLM Activity</button>
2846
+ <button class="tab" data-tab="tokens" onclick="switchTab('tokens')">Tokens</button>
2847
+ <button class="tab" data-tab="resources" onclick="switchTab('resources')">Resource Usage</button>
2848
+ </div>
2849
+ <div class="tab-group" role="group" aria-label="Intelligence & Comms">
2850
+ <div class="tab-group-label">Intelligence &amp; Comms</div>
2851
+ <button class="tab" data-tab="threadline" onclick="switchTab('threadline')">Threadline</button>
2852
+ <button class="tab" data-tab="integrated-being" onclick="switchTab('integrated-being')">Integrated-Being</button>
2853
+ <button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
2854
+ <button class="tab" data-tab="preferences-learning" onclick="switchTab('preferences-learning')">Preferences</button>
2855
+ </div>
2856
+ <div class="tab-group" role="group" aria-label="Content & Access">
2857
+ <div class="tab-group-label">Content &amp; Access</div>
2858
+ <button class="tab" data-tab="files" onclick="switchTab('files')">Files</button>
2859
+ <button class="tab" data-tab="dropzone" onclick="switchTab('dropzone')">Send Content</button>
2860
+ <button class="tab" data-tab="secrets" onclick="switchTab('secrets')">Secrets <span class="tab-count" id="tabSecretCount">0</span></button>
2861
+ <button class="tab" data-tab="features" onclick="switchTab('features')">Features</button>
2862
+ <button class="tab" data-tab="mandates" onclick="switchTab('mandates')">Mandates</button>
2863
+ </div>
2779
2864
  </nav>
2780
2865
  </div>
2781
2866
  <div class="vital-signs" id="vitalSigns">
@@ -2815,6 +2900,7 @@
2815
2900
  <button class="new-session-btn" onclick="openNewSessionModal()" title="New session">+</button>
2816
2901
  </div>
2817
2902
  </div>
2903
+ <div class="tab-purpose" style="padding:0 12px 8px">Your agent's live sessions — pick one to watch its terminal in real time.</div>
2818
2904
  <div class="session-list" id="sessionList">
2819
2905
  <div class="empty-state" id="emptyState">
2820
2906
  <div class="icon">&#x1F4AD;</div>
@@ -2867,13 +2953,14 @@
2867
2953
  <div class="file-tree-header">
2868
2954
  <h2>Files</h2>
2869
2955
  </div>
2956
+ <div class="tab-purpose" style="padding:0 12px">Browse and edit this project's files from any device — changes save straight to disk.</div>
2870
2957
  <div class="file-tree-list" id="fileTreeList">
2871
2958
  <div class="file-loading" id="fileTreeLoading">Loading...</div>
2872
2959
  </div>
2873
2960
  </div>
2874
2961
  <div class="file-content-panel" id="fileContentPanel">
2875
2962
  <div class="file-content-header" id="fileContentHeader" style="display:none">
2876
- <button class="files-back-btn" onclick="filesGoBack()">&larr;</button>
2963
+ <button class="files-back-btn" onclick="filesGoBack()" title="Back" aria-label="Back">&larr;</button>
2877
2964
  <div class="breadcrumbs" id="fileBreadcrumbs"></div>
2878
2965
  <div class="file-badges" id="fileBadges"></div>
2879
2966
  </div>
@@ -2981,7 +3068,7 @@
2981
3068
  <option value="lastRun">Sort: Last Run</option>
2982
3069
  </select>
2983
3070
  </div>
2984
- <div style="font-size:11px;color:var(--text-dim);padding:0 12px 8px;line-height:1.4">Monitor and control your scheduled tasks</div>
3071
+ <div class="tab-purpose" style="padding:0 12px 8px">Monitor and control your scheduled tasks — recurring jobs that run on their own.</div>
2985
3072
  <div id="jobsSummary" class="jobs-summary"></div>
2986
3073
  <div class="jobs-filter-bar" id="jobsFilterBar">
2987
3074
  <button class="jobs-filter-chip active" data-filter="all" onclick="setJobFilter('all')">All</button>
@@ -3012,6 +3099,7 @@
3012
3099
  <button onclick="loadIntegratedBeing()" style="padding:6px 12px">Refresh</button>
3013
3100
  </div>
3014
3101
  </div>
3102
+ <div class="tab-purpose">A cross-session record of what other parts of this agent have been doing — the shared memory that keeps separate conversations aware of each other.</div>
3015
3103
  <div id="ibPerMachineNotice" style="font-size:12px;color:var(--text-dim);display:none">
3016
3104
  Integrated-Being on this machine only — cross-machine ledger visibility is not yet implemented.
3017
3105
  </div>
@@ -3133,6 +3221,7 @@
3133
3221
  <button onclick="loadPrPipeline()" style="padding:6px 12px">Refresh</button>
3134
3222
  </div>
3135
3223
  </div>
3224
+ <div class="tab-purpose">A live view of your open pull requests as they move through review and CI checks toward merge.</div>
3136
3225
  <div id="prPipelinePhaseNotice" style="font-size:12px;color:var(--text-dim)"></div>
3137
3226
  <div id="prPipelineEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3138
3227
  No PR activity to display.
@@ -3146,11 +3235,11 @@
3146
3235
  <h2 style="margin:0">Projects</h2>
3147
3236
  <button onclick="loadProjects()" style="padding:6px 12px">Refresh</button>
3148
3237
  </div>
3149
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3150
- Multi-spec project plans. Each project bundles related child initiatives across rounds with structurally-gated round advance.
3238
+ <div class="tab-purpose">
3239
+ Multi-spec project plans. Each project groups related pieces of work and only advances to its next round once its required checks pass.
3151
3240
  </div>
3152
3241
  <div id="projectsEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3153
- No active projects. Create one via <code>POST /projects</code> with a plan-doc path.
3242
+ No active projects yet. Ask your agent to start one from a plan document, and it'll appear here.
3154
3243
  </div>
3155
3244
  <div id="projectsList" style="display:flex;flex-direction:column;gap:12px"></div>
3156
3245
  </div>
@@ -3173,15 +3262,15 @@
3173
3262
  <button onclick="loadInitiatives()" style="padding:6px 12px">Refresh</button>
3174
3263
  </div>
3175
3264
  </div>
3176
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3177
- Multi-phase long-running work. Signals: <span style="color:#e27d3b">needs-user</span>,
3265
+ <div class="tab-purpose">
3266
+ Long-running work that spans multiple phases, with a status flag on each. Signals: <span style="color:#e27d3b">needs-user</span>,
3178
3267
  <span style="color:#d65f5f">next-check-due</span>,
3179
3268
  <span style="color:#4a9a4a">ready-to-advance</span>,
3180
3269
  <span style="color:#888">stale (&gt;7d)</span>.
3181
3270
  </div>
3182
3271
  <div id="initiativesDigest" style="display:none;padding:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-dim)"></div>
3183
3272
  <div id="initiativesEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3184
- No initiatives to display. Create one via <code>POST /initiatives</code>.
3273
+ No initiatives yet. Ask your agent to create one, and it'll appear here.
3185
3274
  </div>
3186
3275
  <div id="initiativesList" style="display:flex;flex-direction:column;gap:12px"></div>
3187
3276
  </div>
@@ -3195,9 +3284,8 @@
3195
3284
  <button onclick="loadSecrets()" style="padding:6px 12px">Refresh</button>
3196
3285
  </div>
3197
3286
  </div>
3198
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3199
- Secret Drop — one-time-use links for users to submit credentials safely (never through chat).
3200
- Links expire automatically and are destroyed after submission.
3287
+ <div class="tab-purpose">
3288
+ Secret Drop — create one-time links so someone can hand you a password or API key safely, never pasted into chat. Each link expires and is destroyed the moment it's used.
3201
3289
  </div>
3202
3290
  <div id="secretsEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3203
3291
  No pending secret requests.
@@ -3210,7 +3298,7 @@
3210
3298
  <h2 style="margin:0">Commitments</h2>
3211
3299
  <button onclick="loadCommitments()" style="padding:6px 12px">Refresh</button>
3212
3300
  </div>
3213
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3301
+ <div class="tab-purpose">
3214
3302
  Open promises — beacon-watched commitments (⏳). States:
3215
3303
  <span style="color:#4a9a4a">pending</span>,
3216
3304
  <span style="color:#e27d3b">atRisk</span>,
@@ -3228,7 +3316,7 @@
3228
3316
  <h2 style="margin:0">Tokens</h2>
3229
3317
  <button onclick="loadTokens()" style="padding:6px 12px">Refresh</button>
3230
3318
  </div>
3231
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3319
+ <div class="tab-purpose">
3232
3320
  Token usage rolled up from Claude Code session transcripts. Last 24 hours unless noted.
3233
3321
  </div>
3234
3322
 
@@ -3264,7 +3352,7 @@
3264
3352
  <h2 style="margin:0">Resource Usage</h2>
3265
3353
  <button onclick="loadResources()" style="padding:6px 12px">Refresh</button>
3266
3354
  </div>
3267
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3355
+ <div class="tab-purpose">
3268
3356
  How much CPU and memory this agent is using right now — the server process and each running session — sampled continuously. Last hour unless noted.
3269
3357
  </div>
3270
3358
 
@@ -3304,7 +3392,7 @@
3304
3392
  <h2 style="margin:0">Blockers</h2>
3305
3393
  <button onclick="loadBlockers()" style="padding:6px 12px">Refresh</button>
3306
3394
  </div>
3307
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3395
+ <div class="tab-purpose">
3308
3396
  Things that looked like they stopped progress, tracked as <b>decaying hypotheses</b> rather than settled walls.
3309
3397
  Each one moves through verification states; a "true blocker" is the best current understanding with a recheck date —
3310
3398
  never a permanent "give up".
@@ -3330,7 +3418,7 @@
3330
3418
  <button onclick="loadLlmActivity()" style="padding:6px 12px">Refresh</button>
3331
3419
  </div>
3332
3420
  </div>
3333
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3421
+ <div class="tab-purpose">
3334
3422
  Every decision the system's "autopilot" makes with an AI call — sentinels, gates, background checks.
3335
3423
  For each one: which AI provider ran it, how often it actually <b>acted</b> on something vs. found
3336
3424
  nothing to do, how often it was <b>skipped</b> to save on rate limits, and what it cost. A row that
@@ -3364,7 +3452,7 @@
3364
3452
  <h2 style="margin:0">Routing Map</h2>
3365
3453
  <button onclick="loadRoutingMap()" style="padding:6px 12px">Refresh</button>
3366
3454
  </div>
3367
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3455
+ <div class="tab-purpose">
3368
3456
  Where each internal job-kind sends its AI calls. Every background check, gate, and
3369
3457
  sentinel has a <b>lane</b> (its "nature") and a fallback list of <b>doors</b> — the ways
3370
3458
  it can reach a model. This shows, per lane, the ordered door + model list it tries, and
@@ -3407,7 +3495,7 @@
3407
3495
  <button onclick="loadRoutingSpend()" style="padding:6px 12px">Refresh</button>
3408
3496
  </div>
3409
3497
  </div>
3410
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3498
+ <div class="tab-purpose">
3411
3499
  What the internal AI calls cost, and where the paid-door caps sit. The dollar figures are a
3412
3500
  <b>reporting</b> view — immutable token counts priced against a reviewed price list on read, so a
3413
3501
  price correction reflows automatically. <b>No paid door is live yet</b>, so metered spend is $0;
@@ -3642,7 +3730,7 @@
3642
3730
  <h2 style="margin:0">Evidence</h2>
3643
3731
  <div style="font-size:12px;color:var(--text-dim)">Per-entity evidence + reverse citations</div>
3644
3732
  </div>
3645
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3733
+ <div class="tab-purpose">
3646
3734
  Inspect the evidence trail behind any memory entity, or ask "what cites this?" given a kind + sourceId.
3647
3735
  All results are viewer-scope filtered by SemanticMemory before they reach this view.
3648
3736
  </div>
@@ -3701,7 +3789,7 @@
3701
3789
  <h2 style="margin:0">Threadline</h2>
3702
3790
  <button onclick="loadThreadlineBridgeConfig()" style="padding:6px 12px">Refresh</button>
3703
3791
  </div>
3704
- <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
3792
+ <div class="tab-purpose">
3705
3793
  Threadline is your agent's secure channel to other AI agents. The Telegram bridge mirrors those conversations into per-thread Telegram topics so you can watch every exchange. Quiet by default — flip the master switch below to opt in.
3706
3794
  </div>
3707
3795
 
@@ -3798,7 +3886,7 @@
3798
3886
  <h2>Health</h2>
3799
3887
  <button class="systems-refresh" onclick="loadSystems()">Refresh</button>
3800
3888
  </div>
3801
- <div style="font-size:12px;color:var(--text-dim);margin-bottom:16px;line-height:1.4">Your agent's health and running processes</div>
3889
+ <div class="tab-purpose" style="margin-bottom:16px">Your agent's health and running processes — the server and each running session at a glance.</div>
3802
3890
  <div id="systemsBanner">
3803
3891
  <div style="padding:20px;color:var(--text-dim);text-align:center">Loading...</div>
3804
3892
  </div>
@@ -3867,7 +3955,7 @@
3867
3955
  <!-- WhatsApp QR panel (hidden by default) -->
3868
3956
  <div class="wa-qr-backdrop" id="waQrBackdrop" style="display:none" onclick="closeQrPanel()"></div>
3869
3957
  <div class="wa-qr-panel" id="waQrPanel" style="display:none">
3870
- <button class="wa-close" onclick="closeQrPanel()">&times;</button>
3958
+ <button class="wa-close" onclick="closeQrPanel()" title="Close" aria-label="Close">&times;</button>
3871
3959
  <h3>WhatsApp Connection</h3>
3872
3960
  <p id="waQrMsg">Scan this QR code with WhatsApp to connect</p>
3873
3961
  <div id="waQrContainer"></div>
@@ -5537,7 +5625,7 @@
5537
5625
  <span style="font-size:11px;color:var(--text-dim)">confidence: ${escapeHtml(confidence)}</span>
5538
5626
  <button class="ev-inspect-btn" data-entity-id="${id}" style="margin-left:auto;padding:2px 8px;font-size:11px">Inspect evidence</button>
5539
5627
  </div>
5540
- ${content ? `<div style="font-size:12px;color:var(--text-dim);line-height:1.4">${content}</div>` : ''}
5628
+ ${content ? `<div class="tab-purpose">${content}</div>` : ''}
5541
5629
  </div>
5542
5630
  `;
5543
5631
  }
@@ -7571,7 +7659,7 @@
7571
7659
 
7572
7660
  detail.innerHTML = '<div class="job-detail-header">'
7573
7661
  + '<div>'
7574
- + '<button class="back-btn" onclick="jobsGoBack()" style="display:none;margin-right:8px">&larr;</button>'
7662
+ + '<button class="back-btn" onclick="jobsGoBack()" style="display:none;margin-right:8px" title="Back" aria-label="Back">&larr;</button>'
7575
7663
  + '<h3>' + esc(displayName) + '</h3>'
7576
7664
  + '<div class="job-desc">' + esc(job.description || '') + '</div>'
7577
7665
  + '</div>'
@@ -9342,7 +9430,7 @@
9342
9430
  try {
9343
9431
  const data = await apiFetch(info.endpoint);
9344
9432
  panel.innerHTML = '<div class="cap-content-panel">' +
9345
- '<div class="cap-content-header"><span>' + esc(info.label) + '</span><button onclick="document.getElementById(\'capContentBrowser\').innerHTML=\'\'" style="border:none;background:none;color:var(--text-dim);cursor:pointer;font-size:14px">&times;</button></div>' +
9433
+ '<div class="cap-content-header"><span>' + esc(info.label) + '</span><button onclick="document.getElementById(\'capContentBrowser\').innerHTML=\'\'" style="border:none;background:none;color:var(--text-dim);cursor:pointer;font-size:14px" title="Close" aria-label="Close">&times;</button></div>' +
9346
9434
  '<div class="cap-content-body">' + renderBrowsableContent(info.renderer, data) + '</div></div>';
9347
9435
  } catch (e) {
9348
9436
  panel.innerHTML = '<div style="padding:12px;color:var(--red);font-size:12px">Failed to load: ' + esc(e.message) + '</div>';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.787",
3
+ "version": "1.3.789",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-09T03:23:07.095Z",
5
- "instarVersion": "1.3.787",
4
+ "generatedAt": "2026-07-09T07:11:33.686Z",
5
+ "instarVersion": "1.3.789",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,45 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - **All 25 dashboard tabs are now reachable — grouped navigation (Dashboard UX Standard F2).**
9
+ The dashboard grew to 25 tabs in a flat horizontal bar that clipped at ~8 tabs on a 1280px
10
+ viewport with a hidden scrollbar — the other ~17 (Spend, Machines, Mandates, Process Health,
11
+ …) were unreachable by pointer, and the active tab often wasn't even visible in the nav
12
+ (operator report, topic 29723). The nav is now ONE grouped dropdown menu, opened from the
13
+ always-visible `☰` button at every viewport, organized into five labeled sections (Runtime,
14
+ Work & Delivery, Cost & Routing, Intelligence & Comms, Content & Access). It reuses the
15
+ existing (previously mobile-only) collapsible-menu machinery — every tab's data-tab, onclick,
16
+ and count badge is preserved verbatim.
17
+ - **The Dashboard UX Standard is now a constitutional standard with CI-enforced floors.**
18
+ `docs/specs/dashboard-ux-standard.md` (operator-approved, topic 29723) turns the operator's
19
+ "VERY user friendly / EASY to navigate" bar into eight objective floors. Two ship enforced:
20
+ F1 (panel placement, shipped #1403) and F2 (nav reachability, this change) — the rest are
21
+ tracked follow-up passes. Registered in `docs/STANDARDS-REGISTRY.md` so the
22
+ Standards-Enforcement-Coverage audit classifies them as enforced gates.
23
+
24
+ ## What to Tell Your User
25
+
26
+ Every dashboard tab is now reachable from one clean, grouped menu (opened by the ☰ button) at
27
+ desktop and phone width — no more tabs hidden off the edge of the screen. Nothing to enable;
28
+ it lands with the release.
29
+
30
+ ## Summary of New Capabilities
31
+
32
+ - None — this is a navigation/UX fix plus a build-time floor (`dashboard-nav-reachability.test.ts`)
33
+ that fails the build if a future tab is unreachable or the nav loses its grouped structure.
34
+
35
+ ## Evidence
36
+
37
+ `tests/unit/dashboard-nav-reachability.test.ts` — 3 cases: every `TAB_REGISTRY` tab has a
38
+ reachable nav control and there are no orphan nav buttons (population floor ≥20), the nav is
39
+ grouped into labeled sections, and the menu is reachable at all widths (`.nav-toggle`
40
+ `display:flex` at base + `.app.nav-open .tab-bar` open mechanism). Real-browser (headless
41
+ Chromium) verified at 1280×860 and 390×844: the grouped menu renders all five sections with all
42
+ 25 tabs, active-tab highlight + count badges intact, closed-state header shows the `☰ Sessions`
43
+ button and a full-width panel; no horizontal body overflow. Both dashboard floor tests green
44
+ (7 cases). The header-logo audit finding was confirmed a static-render artifact (logo.png is
45
+ served live via `express.static`) — corrected in the spec, no spurious fix made.
@@ -0,0 +1,44 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - **Every dashboard control is now labeled (Dashboard UX Standard F5).** Four icon-only buttons
9
+ (the Files back arrow, the QR-panel close, the Jobs back arrow, and the capability-content
10
+ close) shipped as a bare `←` / `×` glyph with no accessible name — a user (or screen reader)
11
+ had to guess. Each now carries a `title` + `aria-label`. A build-time floor
12
+ (`dashboard-controls-labeled.test.ts`) fails if any future button ships icon-only.
13
+ - **Empty states speak plain language, not API jargon (F6).** Two empty states told the operator
14
+ to "Create one via `POST /projects`" / "`POST /initiatives`" — meaningless to the person
15
+ reading the dashboard. They now say "Ask your agent to start one … and it'll appear here." A
16
+ floor (`dashboard-empty-states.test.ts`) fails on a bare or jargon-leaking empty state.
17
+ - **Shared purpose-line vocabulary documented (F7, soft-first).** `.tab-purpose` is marked the
18
+ canonical purpose-line class; the three legacy variants (`ph-intro`, `features-subtitle`,
19
+ `dropzone-subtitle`) are documented for a tracked, deliberate migration (per spec FD-3, not a
20
+ big-bang restyle).
21
+ - **Broken-asset floor (F8).** `dashboard-assets-resolve.test.ts` fails if a referenced local
22
+ image/icon has no file on disk.
23
+
24
+ ## What to Tell Your User
25
+
26
+ A few more dashboard papercuts are gone: every button now has a readable label (better for
27
+ accessibility too), and the "nothing here yet" messages tell you what to do in plain English
28
+ instead of showing a developer command. Nothing to enable — it lands with the release.
29
+
30
+ ## Summary of New Capabilities
31
+
32
+ - None — UX polish plus three build-time floors (F5 control labeling, F6 empty-state clarity,
33
+ F8 asset resolution) that fail the build if a future change regresses them.
34
+
35
+ ## Evidence
36
+
37
+ `tests/unit/dashboard-controls-labeled.test.ts` (F5): scans every `<button>` in
38
+ `dashboard/index.html`, asserts each has visible text, `title`, or `aria-label` (population floor
39
+ ≥30). `tests/unit/dashboard-empty-states.test.ts` (F6): audits the `*Empty*` resting-state
40
+ containers (population floor ≥6) for a real sentence and no `POST /…`/curl jargon.
41
+ `tests/unit/dashboard-assets-resolve.test.ts` (F8): every static local `src`/`url(...)` ref
42
+ resolves to a real file (logo.png confirmed present). All 7 dashboard floor tests green (13
43
+ cases, 1 browser-gated skip). Display-only markup + build-time floors — no runtime surface;
44
+ machine-local static asset; rollback is a plain revert.
@@ -0,0 +1,20 @@
1
+ # Side-Effects Review — Dashboard F3 (tab purpose lines)
2
+
3
+ **Change:** Implements floor **F3** of the Dashboard UX Standard (`docs/specs/dashboard-ux-standard.md`, merged in #1404): every registered dashboard tab now carries a plain-language, self-explanatory purpose line near its top. Adds a shared `.tab-purpose` CSS class, converts the dashboard's pre-existing muted intro divs to it (de-jargoning the worst offenders), adds purpose lines to the tabs that lacked one (Sessions, Files), and adds a static floor test.
4
+
5
+ **Files:**
6
+ - `dashboard/index.html` — one `.tab-purpose` CSS rule; ~15 panels' intro divs converted to (or given) `.tab-purpose`; new purpose lines for Sessions and Files. Pure markup/CSS — no script changes.
7
+ - `tests/unit/dashboard-tab-purpose.test.ts` — the F3 floor: maps every `TAB_REGISTRY` tab to its panel markup and fails (naming the tab) if none carries a recognized purpose-line class (`tab-purpose` / `ph-intro` / `features-subtitle` / `dropzone-subtitle`). Population-floor guarded.
8
+
9
+ **Side effects / blast radius:**
10
+ - **Runtime behavior:** NONE. The dashboard is a static file served by `express.static`; this change adds display-only markup + CSS. No JS logic, no API routes, no state, no config touched.
11
+ - **Migrations:** none required. The dashboard `index.html` is served from the package, not templated per-agent; no `PostUpdateMigrator` / CLAUDE.md-template surface is involved (no agent-installed file changes).
12
+ - **Server/lifeline/deploy path:** untouched.
13
+ - **Cross-machine / mesh:** untouched.
14
+ - **Security/PII:** none — no data, credentials, or user content flow through the changed lines.
15
+
16
+ **Risk:** Low. Worst case is a cosmetic mis-placement of a purpose line on a tab, visible only in the dashboard UI, fixable by a follow-up edit. The floor test prevents a future tab from silently shipping without a purpose line.
17
+
18
+ **Rollback:** revert this commit — the dashboard reverts to the prior markup with zero residual state (nothing persisted, nothing migrated).
19
+
20
+ **Follow-ups (tracked, out of scope here):** F4 (mobile no-horizontal-scroll) ships next; F5–F8 (labeled controls, self-explaining empty states, shared component vocabulary that will consolidate the 4 accepted purpose-line classes into one) follow per the spec sequencing.
@@ -0,0 +1,18 @@
1
+ # Side-Effects Review — Dashboard F4 (no mobile horizontal scroll)
2
+
3
+ **Change:** Implements floor **F4** of the Dashboard UX Standard (`docs/specs/dashboard-ux-standard.md`): the page body must never scroll horizontally at 1280/768/390px. The audit (2026-07-08) found panels overflowing the viewport's right edge at 390px so the whole page scrolled sideways on a phone.
4
+
5
+ **Files:**
6
+ - `dashboard/index.html` — adds `min-width: 0` to the four grid-child panel containers that lacked it (`files-container`, `jobs-container`, `systems-container`, `features-container`). These are direct `.app` grid children (`grid-column: 1 / -1`); a CSS grid/flex child defaults to `min-width: auto`, so a wide descendant forces the row wider than the viewport and the body scrolls sideways. `min-width: 0` lets the child shrink so wide content wraps/contains instead of blowing out the row. (`.tab-panel` already carried this from #1403.) No JS touched.
7
+ - `tests/unit/dashboard-viewport-scroll.test.ts` — the F4 floor, in two halves: (1) a STATIC check that every grid-child container carries `min-width: 0` (always runs); (2) a BROWSER-GATED render check that `document.body.scrollWidth <= clientWidth` at each viewport, which SKIPS HONESTLY when no browser driver is installed (FD-2) and runs in the CI browser job. Never a false green.
8
+
9
+ **Side effects / blast radius:**
10
+ - **Runtime behavior:** NONE. Display-only CSS on static markup. No JS, API, state, or config.
11
+ - **Migrations / agent-installed files:** none (dashboard is package-served, not templated per agent).
12
+ - **Server/lifeline/mesh/security:** untouched.
13
+
14
+ **Risk:** Very low. `min-width: 0` on a grid child is the textbook, side-effect-free fix for grid row-blowout; it only *allows* a child to shrink, it never forces a size. Worst case is a wide element inside one of these tabs wrapping instead of overflowing — the desired behavior.
15
+
16
+ **Verification honesty:** The static half is verified green here. The rendered half is **browser-gated and was SKIPPED in this worktree** — no Playwright/Puppeteer is installed, and installing a browser (a large download) was deliberately avoided on a host that wedged twice on 2026-07-08 under filesystem load. The `min-width: 0` fix targets the known mechanism (grid child blowout) with high confidence from the CSS, but a pixel-level 390px confirmation should run via the CI browser job or a browser-capable pass before treating F4 as fully closed. This is disclosed rather than claimed as visually verified.
17
+
18
+ **Rollback:** revert — pure CSS + a test, zero residual state.
@@ -0,0 +1,22 @@
1
+ # Side-effects review — Dashboard UX Standard floors F5–F8
2
+
3
+ **Change:** `dashboard/index.html` (F5 button labels, F6 empty-state de-jargoning, F7 canonical-class doc) + three static floor tests (`dashboard-controls-labeled` F5, `dashboard-empty-states` F6, `dashboard-assets-resolve` F8). Spec: `docs/specs/dashboard-ux-standard.md` (converged + approved, shipped in #1404). Tier 1 — display-only markup + build-time CI floors; no runtime `src/` change, no decision logic.
4
+
5
+ ## Phase 1 — Principle check (signal vs authority)
6
+ Does this touch a decision point that gates information flow / blocks actions / constrains agent behavior? **No.** The changes are (a) HTML label/text attributes rendered to the operator and (b) static test files that fail a *build* if the dashboard regresses. The tests are build-time CI floors, not runtime authorities — they never gate a message, session, or agent action. Signal-vs-authority does not apply (no runtime decision surface).
7
+
8
+ ## Phase 4 — Side-effects review
9
+ 1. **Over-block:** the floor tests could reject legitimate markup. F5 accepts a button labeled by visible text OR `title` OR `aria-label` OR dynamic (`${…}`/concatenated) content — the full current button population passes; a button labeled *only* by an adjacent element (not itself) would false-flag, acceptable because self-labeling is the accessibility norm and easily satisfied. F6 requires a ≥4-word sentence in each `*Empty*` container — a legitimately terse state would flag, but the 4-word floor is lenient. F8 excludes `data:`/`http(s)`/dynamic `${}` srcs, so only real static local refs are checked. No legitimate current markup is rejected (all 7 dashboard tests green).
10
+ 2. **Under-block:** F5 checks `<button>` only, not `<select>`/`<input>` labeling (a tracked F5 follow-up <!-- tracked: topic-29723 -->). F6 audits `*Empty*`-id containers, not every conceivable resting state. F8 checks static local refs only. These are honest partial floors that raise the bar without claiming totality — not regressions.
11
+ 3. **Level-of-abstraction fit:** correct layer. A UI-consistency standard is enforced at build time over the shipped markup; there is no smarter runtime gate this should feed (it is not a runtime concern). It extends the existing dashboard-floor test family (`dashboard-tab-purpose`, `dashboard-panel-placement`, `dashboard-nav-reachability`, `dashboard-viewport-scroll`) — same pattern, same layer.
12
+ 4. **Signal vs authority compliance:** compliant by absence — no blocking authority added (build-time test, not runtime gate). See Phase 1.
13
+ 5. **Interactions:** the new F5/F6/F8 tests are independent scans over `index.html`; none shadows another. The F7 change is a CSS *comment* only — it does not alter the `.tab-purpose` rule and the F3 `dashboard-tab-purpose` test (which recognizes all four purpose-line classes) still passes. No double-fire, no race with cleanup.
14
+ 6. **External surfaces:** changes the dashboard HTML the operator sees — 4 icon buttons gain accessible names, 2 empty states lose API jargon ("POST /projects" → plain language). Strictly additive/clarifying; no behavior, endpoint, or data change. No surface visible to other agents/users/systems. No timing/conversation-state dependency.
15
+ 7. **Multi-machine posture:** **machine-local BY DESIGN** — `dashboard/index.html` is a static asset shipped in the package and served by each machine's own server; every machine serves the identical shipped file, so there is nothing to replicate, proxy, or transfer. No durable state, no generated URL, no user-facing notice. A single-machine assumption is correct here, not a defect.
16
+ 8. **Rollback cost:** trivial — `git revert` the commit. Display-only markup + additive tests; no migration, no agent-state repair, no data change.
17
+
18
+ ## Phase 4.5 — No deferrals
19
+ The two under-block follow-ups (F5 select/input labeling; broader F6 coverage) are tracked <!-- tracked: topic-29723 -->. F7 hard-consolidation is deliberately soft-first per the spec's FD-3, not an orphan deferral. No partial fix of an in-scope item is shipped.
20
+
21
+ ## Phase 5 — Second pass
22
+ Not required: no block/allow, session-lifecycle, coherence, or sentinel/guard/gate/watchdog surface (Tier 1, display-only).
@@ -0,0 +1,53 @@
1
+ # Side-Effects Review — Grouped-nav reachability + Dashboard UX Standard (F1/F2)
2
+
3
+ **Version / slug:** `dashboard-ux-standard-nav`
4
+ **Date:** `2026-07-08`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Makes all 25 dashboard tabs reachable (operator report, topic 29723: 17 of 25 unreachable in a clipping flat nav bar) by converting the nav into ONE grouped dropdown menu, opened from the always-visible `☰` toggle at every viewport, organized into five labeled sections. This reuses the existing (previously mobile-only) `.app.nav-open .tab-bar` collapsible-menu machinery — `toggleNavMenu`/`closeNavMenu`/`updateNavToggleLabel`/`switchTab` are untouched; every tab button's `data-tab`, `onclick`, and count badge is preserved verbatim (25 in, 25 out, cross-checked against `TAB_REGISTRY`). Ships the operator-approved `docs/specs/dashboard-ux-standard.md` (8 floors), a `docs/STANDARDS-REGISTRY.md` entry, and the F2 floor test. Files touched: `dashboard/index.html` (CSS: `.nav-toggle` visible at base + `.tab-bar` → grouped dropdown + `.tab-group`/`.tab-group-label`; markup: tabs wrapped in groups), `tests/unit/dashboard-nav-reachability.test.ts`, `docs/specs/dashboard-ux-standard.md`, `docs/STANDARDS-REGISTRY.md`, this artifact, a release fragment.
11
+
12
+ ## Decision-point inventory
13
+
14
+ No runtime decision-point surface. Presentation-layer CSS/HTML plus a static test. It gates no information flow, blocks no actions, filters no messages, constrains no agent behavior at runtime. The new unit test constrains future COMMITS (the standard test-ratchet pattern), not runtime behavior.
15
+
16
+ ---
17
+
18
+ ## 1. Over-block
19
+
20
+ No block/allow surface at runtime. For the commit-time ratchet: the F2 floor could "over-block" a future legitimate nav change — e.g. a deliberate move to a different reachable model (persistent left rail). Mitigation: the floor asserts the FLOOR (every tab reachable, nav grouped, reachable at all widths), not a specific implementation; a left-rail that keeps tabs reachable + grouped still passes. Failure messages name the exact unreachable/orphan tab and the fix.
21
+
22
+ ## 2. Under-block
23
+
24
+ The floor keys on `data-tab="..."` buttons inside the `id="tabBar"` nav and `id: '...'` entries in the `TAB_REGISTRY` array. A tab injected purely from JS (not in the static markup) or a registry authored with unconventional syntax would not be seen. Accepted: the regression vector this closes is the observed flat-nav clip (a real 25-tab overflow); the population floor (≥20 nav buttons AND ≥20 registry ids visible) fails loudly if either matcher goes blind. Deeper enforcement (the F4 viewport smoke test for body-never-h-scroll) is a tracked follow-up pass in the spec's implementation sequencing. <!-- tracked: topic-29723 dashboard-ux-brief Increment 2b -->
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ Right layer. The bug is nav reachability; the fix reuses the existing collapsible-menu mechanism at the layout layer rather than inventing new nav architecture (a persistent sidebar rewrite would relocate the sessions panel that owns the 280px column — higher risk for no reachability gain). No smarter existing gate applies (not decision logic).
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ Compliant by vacuity at runtime: the change holds no blocking authority and produces no signal — inert presentation markup. The unit test is a build-time ratchet with commit-blocking authority through CI exactly like every other test, with deterministic human-readable failure output.
33
+
34
+ ## 5. Interactions
35
+
36
+ - `switchTab()` already closes the menu (`classList.remove('nav-open')`) and updates the toggle label on selection — verified; the always-dropdown model needs no JS change.
37
+ - The mobile `@media` block still sets `.tab-bar` full-width (`left:0;right:0`) — it reinforces the base dropdown harmlessly (mobile menu = full width).
38
+ - `.tab-panel` placement (F1, #1403) is unaffected — this change touches only the header nav, not the after-main panels.
39
+ - Fresh class names (`.tab-group`, `.tab-group-label`) — grep-verified no prior definitions; `.nav-toggle`/`.tab-bar` rules are the only ones restyled, both verified.
40
+
41
+ ## 6. External surfaces
42
+
43
+ Visible change: the dashboard nav becomes a grouped `☰` menu reachable at every width; all 25 tabs reachable (was ~8). No API change, no data change, no cross-agent surface, no timing/conversation-state dependency. Verified with real-browser (headless Chromium) at 1280×860 (grouped menu: 5 sections, all tabs, active highlight + count badges) and 390×844 (full-width menu, no h-overflow), plus the closed default state (header `☰ Sessions` button + full-width panel).
44
+
45
+ **Operator-Surface-Quality (dashboard is an operator surface):** (1) leads with the primary action — the nav is the surface's primary affordance and is now front-and-center, all destinations reachable; (2) zero raw internals — tabs are plain-language labels, grouped under plain-language section headers, no JSON/IDs/slugs; (3) no destructive action in the nav; (4) plain language a non-engineer reads ("Cost & Routing → Spend"); (5) works at phone width — 40px+ tap targets, readable 14–15px type, no horizontal scroll (headless-verified at 390px). The header-logo "broken" audit finding was confirmed a static-render false positive (logo.png served live via `express.static`) — no spurious change made.
46
+
47
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
48
+
49
+ Machine-local BY DESIGN like all dashboard markup: the HTML ships identically to every machine with the release; each serves its own copy. No replicated state, no machine-boundary URL, no one-voice notice surface. Both operator machines get it as they pick up the release.
50
+
51
+ ## 8. Rollback cost
52
+
53
+ Trivial: revert the commit (CSS + nav markup regroup + one test + the spec/registry/fragment docs). No data migration, no agent state, no config. A revert restores the prior (clipping) nav and deletes the F2 floor with it.