create-pmos 0.80.0 → 0.82.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.80.0",
3
+ "version": "0.82.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@f4177c6
1
+ kernel-source: pmos@a09f396
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T13:06:30Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T15:34:34Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-07T13:06:30Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T15:34:34Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1104,8 +1104,8 @@ function computeWorkflowDerived(id, opts) {
1104
1104
  steps.push(step('accept', 'accept', 'blocked', `blocked by: ${blockers.join(', ')}`,
1105
1105
  'the Acceptance Gate is the PM\'s (D12) — but it does not open over an unfinished ladder'));
1106
1106
  } else {
1107
- steps.push(step('accept', 'accept', 'pending', 'PM Acceptance Gate open — non-delegable (D12)',
1108
- 'the PM names one thing verified BY HAND in the acceptance note, then appends to acceptances.jsonl'));
1107
+ steps.push(step('accept', 'accept', 'pending', 'no acceptance record needed (D94)',
1108
+ 'merge once the required checks are green and the PM has not returned the run'));
1109
1109
  }
1110
1110
  }
1111
1111
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-07T13:06:30Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T15:34:34Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -11,11 +11,13 @@ On a desktop screen the workspace is one frame. It has one top bar, the place on
11
11
 
12
12
  ## The top bar
13
13
 
14
+ Corners are rounded a little: 6 pixels on buttons and fields, 8 pixels on cards and sheets. Buttons and tabs use normal capital letters. Only the small headings above a list stay in capitals. The page does not scroll. Only the place on screen scrolls, and the top bar stays where it is. The tab you are on is marked with a straight line under it.
15
+
14
16
  The top bar has these parts, from left to right:
15
17
 
16
18
  - **Repository ▾** names the current repository. Press it to choose another repository, or **All repositories** to see the work of every repository at once. All repositories replaces the old Portfolio mode.
17
19
  - **Branches and sessions** is in ⌘K. It opens a sheet with every branch of the repository (main first) and how many sessions run in each. Press a branch to see its sessions, open one, end one, or start a session there. **+ branch** makes a branch, and **+ session** starts one with a branch, an agent and a profile. With All repositories, the sheet lists every repository.
18
- - **Agenda, Initiatives, Board, Reports, Docs and People** are the six places. Press one to show it. The Agenda count shows how many items need you. Docs shows whether each document is the same as its Google Docs and Confluence copy. People shows the people you work with in the repository ([People](people.md)).
20
+ - **Agenda, My work, Initiatives, Board, Reports, Docs and People** are the places. Each has an icon before its name, the same icon as on the phone. Press one to show it. The Agenda count shows how many items need you. Docs shows whether each document is the same as its Google Docs and Confluence copy. People shows the people you work with in the repository ([People](people.md)).
19
21
  - **Search or capture ⌘K** opens the command palette. Type to find an initiative, a repository, an open tab or a command.
20
22
  - **Sync** shows only when the current repository is not in sync with its remote. Press it to fetch, bring in the work of the other machine, record your changes and push. If the repository has not fetched in the last 10 minutes, Sync also shows, because a push from another machine is not visible until a fetch. When the repository fetched recently and is in sync, Sync is not shown. When two machines add lines to the same ledger (a `.jsonl` file under `pmos/` or `planning/`), Sync keeps the lines from both machines. If Sync stops, the message names the step that stopped. If both machines changed the same lines of another file, Sync changes nothing and names the files. Then choose **Push this machine’s version** or **Pull the other machine’s version**. Sync keeps that version of those files, keeps all other changes from both machines, and finishes. It never overwrites the remote history.
21
23
  - **Sync on its own.** If you forget to press Sync, the workspace syncs for you. It syncs each repository once when the workspace starts. It also syncs a repository that has records not yet pushed when no record changed for 5 minutes. In a document repository it syncs every changed file on any branch. In a code repository it syncs only on the default branch (for example `main`), and only when every commit not yet pushed changes records and no code. If you close the tab while records are not yet pushed, the browser asks you first. To turn automatic sync off, start the workspace with `PMOS_AUTO_SYNC=0`.
@@ -1104,8 +1104,8 @@ function computeWorkflowDerived(id, opts) {
1104
1104
  steps.push(step('accept', 'accept', 'blocked', `blocked by: ${blockers.join(', ')}`,
1105
1105
  'the Acceptance Gate is the PM\'s (D12) — but it does not open over an unfinished ladder'));
1106
1106
  } else {
1107
- steps.push(step('accept', 'accept', 'pending', 'PM Acceptance Gate open — non-delegable (D12)',
1108
- 'the PM names one thing verified BY HAND in the acceptance note, then appends to acceptances.jsonl'));
1107
+ steps.push(step('accept', 'accept', 'pending', 'no acceptance record needed (D94)',
1108
+ 'merge once the required checks are green and the PM has not returned the run'));
1109
1109
  }
1110
1110
  }
1111
1111
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.80.0",
3
+ "version": "0.82.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.80.0",
9
+ "version": "0.82.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.80.0",
3
+ "version": "0.82.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  * the shell still comes up.
31
31
  */
32
32
 
33
- import { el, clear, setChildren } from './dom.js';
33
+ import { el, clear, setChildren, keepScroll } from './dom.js';
34
34
  import { state, setState, subscribe, flush, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
35
35
  /* The phone surface (≤760px). It owns its own chrome and decides which single panel is visible;
36
36
  * the shell's only part in it is to keep all four hosts mounted below the breakpoint and to hand
@@ -563,6 +563,8 @@ let frDetail = null;
563
563
  const hostMain = el('div', { class: 'fr-main', 'data-fr': 'main' });
564
564
  const destHosts = {};
565
565
  for (const id of FRAME_DESTS) destHosts[id] = el('div', { class: 'fr-dest', 'data-fr-host': id, hidden: true });
566
+ // each place keeps its reading position when it redraws through a short state (dom.js keepScroll)
567
+ for (const id of FRAME_DESTS) keepScroll(destHosts[id]);
566
568
  const frEnd = el('span', { class: 'fr-end', 'aria-hidden': 'true' });
567
569
 
568
570
  function setupFrame() {
@@ -5,7 +5,9 @@
5
5
  * primitives `.pmc-*` — written only by primitives.js and icons.js (R28-9)
6
6
  * layouts `.bd-*` — the Breakdown's own arrangement of those primitives
7
7
  * Every selector starts with `.pmc-` or `.bd-`. Colours are tokens.css variables only; there is no hex and no oklch here.
8
- * Radius is 0 (a dot is the one round shape). Hover rules sit inside (hover: hover). Under a coarse pointer every
8
+ * Radius (R27 as amended by the PM 2026-10-07): 6px on controls (buttons, inputs, selects, tabs, segmented controls,
9
+ * badges, menu items, checkboxes), 8px on cards, sheets, dialogs, menus, tiles and panels; a dot (50% or a pill) stays round.
10
+ * Buttons, tabs and segmented controls use sentence case; small non-interactive eyebrows may stay uppercase at 0.04em. Hover rules sit inside (hover: hover). Under a coarse pointer every
9
11
  * control is at least 44px and every row and card at least 48px.
10
12
  */
11
13
 
@@ -53,15 +55,13 @@
53
55
  min-height: 32px;
54
56
  padding: 0 14px;
55
57
  border: 1px solid var(--border);
56
- border-radius: 0;
58
+ border-radius: 6px;
57
59
  background: transparent;
58
60
  color: var(--foreground);
59
61
  font-family: var(--sans);
60
62
  font-size: 12px;
61
63
  line-height: 1;
62
- font-weight: 600;
63
- letter-spacing: 0.1em;
64
- text-transform: uppercase;
64
+ font-weight: 500;
65
65
  white-space: nowrap;
66
66
  cursor: pointer;
67
67
  user-select: none;
@@ -128,6 +128,7 @@
128
128
  display: none;
129
129
  }
130
130
  .pmc-field {
131
+ position: relative;
131
132
  display: flex;
132
133
  flex-direction: column;
133
134
  gap: 2px;
@@ -141,6 +142,8 @@
141
142
  }
142
143
  .pmc-field__label--hidden {
143
144
  position: absolute;
145
+ top: 0;
146
+ left: 0;
144
147
  width: 1px;
145
148
  height: 1px;
146
149
  overflow: hidden;
@@ -158,7 +161,7 @@
158
161
  padding: 0;
159
162
  border: 0;
160
163
  border-bottom: 1px solid var(--input);
161
- border-radius: 0;
164
+ border-radius: 6px;
162
165
  background: transparent;
163
166
  color: var(--foreground);
164
167
  font-family: var(--sans);
@@ -218,7 +221,7 @@
218
221
  height: 16px;
219
222
  margin: 0;
220
223
  border: 1px solid var(--muted-foreground);
221
- border-radius: 0;
224
+ border-radius: 6px;
222
225
  background: transparent;
223
226
  cursor: pointer;
224
227
  }
@@ -249,7 +252,7 @@
249
252
  font-size: 11px;
250
253
  line-height: 16px;
251
254
  font-weight: 600;
252
- letter-spacing: 0.05em;
255
+ letter-spacing: 0.04em;
253
256
  text-transform: uppercase;
254
257
  white-space: nowrap;
255
258
  color: var(--muted-foreground);
@@ -452,7 +455,7 @@
452
455
  min-width: 0;
453
456
  min-height: 36px;
454
457
  padding: 10px 12px;
455
- border-radius: 0;
458
+ border-radius: 8px;
456
459
  background: var(--card);
457
460
  color: var(--foreground);
458
461
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--foreground) 9%, transparent),
@@ -553,14 +556,14 @@
553
556
  min-height: 26px;
554
557
  padding: 0 12px;
555
558
  border: 1px solid transparent;
556
- border-radius: 0;
559
+ border-radius: 6px;
557
560
  background: transparent;
558
561
  color: color-mix(in oklab, var(--foreground) 68%, transparent);
559
562
  font-family: var(--sans);
560
563
  font-size: 12px;
561
564
  line-height: 1;
562
565
  font-weight: 600;
563
- letter-spacing: 0.06em;
566
+ letter-spacing: 0.04em;
564
567
  text-transform: uppercase;
565
568
  white-space: nowrap;
566
569
  cursor: pointer;
@@ -599,6 +602,7 @@
599
602
  }
600
603
  .pmc-tabs--line .pmc-tabs__tab[aria-selected="true"] {
601
604
  background: transparent;
605
+ border-radius: 0;
602
606
  box-shadow: inset 0 -2px 0 var(--foreground);
603
607
  }
604
608
 
@@ -615,7 +619,7 @@
615
619
  overflow: auto;
616
620
  padding: 4px;
617
621
  border: 0;
618
- border-radius: 0;
622
+ border-radius: 8px;
619
623
  background: var(--card);
620
624
  color: var(--foreground);
621
625
  font-family: var(--sans);
@@ -633,7 +637,7 @@
633
637
  min-height: 32px;
634
638
  padding: 0 12px;
635
639
  border: 0;
636
- border-radius: 0;
640
+ border-radius: 6px;
637
641
  background: transparent;
638
642
  color: var(--foreground);
639
643
  font: inherit;
@@ -663,7 +667,7 @@
663
667
  margin: 0;
664
668
  padding: 0;
665
669
  border: 0;
666
- border-radius: 0;
670
+ border-radius: 8px;
667
671
  background: var(--card);
668
672
  color: var(--foreground);
669
673
  font-family: var(--sans);
@@ -794,7 +798,7 @@
794
798
  min-height: 44px;
795
799
  padding: 4px 4px 4px 16px;
796
800
  border: 0;
797
- border-radius: 0;
801
+ border-radius: 8px;
798
802
  background: var(--card);
799
803
  color: var(--foreground);
800
804
  font-family: var(--sans);
@@ -936,7 +940,7 @@
936
940
  margin: 0;
937
941
  padding: 0;
938
942
  border: 0;
939
- border-radius: 0;
943
+ border-radius: 6px;
940
944
  background: transparent;
941
945
  color: inherit;
942
946
  transform: translateY(-50%);
@@ -1084,7 +1088,7 @@
1084
1088
  margin-right: 10px;
1085
1089
  font-size: 12px;
1086
1090
  font-weight: 600;
1087
- letter-spacing: 0.08em;
1091
+ letter-spacing: 0.04em;
1088
1092
  text-transform: uppercase;
1089
1093
  }
1090
1094
  .bd-entry__meta {
@@ -1200,7 +1204,7 @@
1200
1204
  font-size: 11px;
1201
1205
  line-height: 16px;
1202
1206
  font-weight: 600;
1203
- letter-spacing: 0.05em;
1207
+ letter-spacing: 0.04em;
1204
1208
  text-transform: uppercase;
1205
1209
  color: var(--muted-foreground);
1206
1210
  }
@@ -1287,7 +1291,7 @@
1287
1291
  font-size: 12px;
1288
1292
  line-height: 16px;
1289
1293
  font-weight: 600;
1290
- letter-spacing: 0.06em;
1294
+ letter-spacing: 0.04em;
1291
1295
  text-transform: uppercase;
1292
1296
  }
1293
1297
  .bd-col__n {
@@ -1545,10 +1549,9 @@
1545
1549
  .in-head__title {
1546
1550
  flex: 1 1 auto;
1547
1551
  margin: 0;
1548
- font-size: 16px;
1549
- line-height: 24px;
1550
- font-weight: 600;
1551
- letter-spacing: -0.01em;
1552
+ font-size: 20px;
1553
+ line-height: 28px;
1554
+ font-weight: 500;
1552
1555
  }
1553
1556
  .in-body {
1554
1557
  display: flex;
@@ -1645,7 +1648,7 @@
1645
1648
  font-size: 12px;
1646
1649
  line-height: 16px;
1647
1650
  font-weight: 600;
1648
- letter-spacing: 0.06em;
1651
+ letter-spacing: 0.04em;
1649
1652
  text-transform: uppercase;
1650
1653
  font-variant-numeric: tabular-nums;
1651
1654
  }
@@ -1670,7 +1673,7 @@
1670
1673
  font-size: 12px;
1671
1674
  line-height: 16px;
1672
1675
  font-weight: 600;
1673
- letter-spacing: 0.06em;
1676
+ letter-spacing: 0.04em;
1674
1677
  text-transform: uppercase;
1675
1678
  }
1676
1679
  .in-col__count {
@@ -1910,7 +1913,7 @@
1910
1913
  font-size: 12px;
1911
1914
  line-height: 16px;
1912
1915
  font-weight: 600;
1913
- letter-spacing: 0.06em;
1916
+ letter-spacing: 0.04em;
1914
1917
  text-transform: uppercase;
1915
1918
  }
1916
1919
  .in-starters {
@@ -2133,7 +2136,7 @@
2133
2136
  margin: 0;
2134
2137
  font-size: 12px;
2135
2138
  font-weight: 600;
2136
- letter-spacing: 0.1em;
2139
+ letter-spacing: 0.04em;
2137
2140
  text-transform: uppercase;
2138
2141
  color: var(--muted-foreground);
2139
2142
  }
@@ -2222,6 +2225,7 @@
2222
2225
  }
2223
2226
  .fr-link[aria-current="page"] {
2224
2227
  color: var(--foreground);
2228
+ border-radius: 0;
2225
2229
  box-shadow: inset 0 -2px 0 var(--foreground);
2226
2230
  }
2227
2231
  .fr-mark {
@@ -2266,6 +2270,8 @@
2266
2270
  min-width: 0;
2267
2271
  min-height: 0;
2268
2272
  overflow-y: auto;
2273
+ /* the destination is the one thing that scrolls: a wheel at its end does not move the page and its bars */
2274
+ overscroll-behavior: contain;
2269
2275
  }
2270
2276
  .fr-dest[hidden] {
2271
2277
  display: none;
@@ -2476,7 +2482,7 @@
2476
2482
  color: var(--muted-foreground);
2477
2483
  font-size: 11px;
2478
2484
  font-weight: 600;
2479
- letter-spacing: 0.1em;
2485
+ letter-spacing: 0.04em;
2480
2486
  text-transform: uppercase;
2481
2487
  text-overflow: ellipsis;
2482
2488
  white-space: nowrap;
@@ -2765,7 +2771,7 @@
2765
2771
  color: var(--muted-foreground);
2766
2772
  font-size: 11px;
2767
2773
  font-weight: 600;
2768
- letter-spacing: 0.1em;
2774
+ letter-spacing: 0.04em;
2769
2775
  text-transform: uppercase;
2770
2776
  list-style: none;
2771
2777
  cursor: pointer;
@@ -3007,7 +3013,7 @@
3007
3013
  color: var(--muted-foreground);
3008
3014
  font-size: 11px;
3009
3015
  font-weight: 600;
3010
- letter-spacing: 0.1em;
3016
+ letter-spacing: 0.04em;
3011
3017
  text-transform: uppercase;
3012
3018
  }
3013
3019
  .fr-rail__graph {
@@ -3089,7 +3095,7 @@
3089
3095
  color: var(--muted-foreground);
3090
3096
  font-size: 11px;
3091
3097
  font-weight: 600;
3092
- letter-spacing: 0.1em;
3098
+ letter-spacing: 0.04em;
3093
3099
  text-transform: uppercase;
3094
3100
  }
3095
3101
  .fr-rail__finding[data-status="stale"] .fr-rail__finding-s,
@@ -3245,7 +3251,7 @@
3245
3251
  .td-day__warn { color: var(--warning); }
3246
3252
  .td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
3247
3253
  .td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
3248
- .td-day__ts { gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
3254
+ .td-day__ts { border-radius: 6px; gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
3249
3255
  .td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
3250
3256
  .td-day__ts[data-step-state="now"] { background: var(--muted); }
3251
3257
  .td-day__ts[aria-current="true"] { border-color: var(--ring); }
@@ -3260,14 +3266,15 @@
3260
3266
  .td-day__run--failed { color: var(--destructive); }
3261
3267
  .td-day__note { margin: 0; font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
3262
3268
 
3263
- .td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: start; }
3264
- .td-day__left { display: grid; gap: 12px; min-width: 0; }
3269
+ .td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: stretch; }
3270
+ .td-day__left { display: grid; gap: 12px; min-width: 0; align-content: start; }
3265
3271
  .td-day__panel, .td-day__detail {
3266
3272
  display: grid;
3267
3273
  gap: 8px;
3268
3274
  min-width: 0;
3269
3275
  padding: 10px 12px;
3270
3276
  border: 1px solid var(--border);
3277
+ border-radius: 8px;
3271
3278
  background: var(--card);
3272
3279
  }
3273
3280
  .td-day__detail { position: sticky; top: 8px; max-height: calc(100vh - 16px); overflow: auto; align-content: start; }
@@ -3282,7 +3289,7 @@
3282
3289
  margin: 0;
3283
3290
  font-size: 12px;
3284
3291
  font-weight: 600;
3285
- letter-spacing: 0.06em;
3292
+ letter-spacing: 0.04em;
3286
3293
  text-transform: uppercase;
3287
3294
  color: var(--muted-foreground);
3288
3295
  }
@@ -3295,7 +3302,7 @@
3295
3302
  .td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
3296
3303
  .td-day__chips { display: inline-flex; gap: 4px; }
3297
3304
  .td-day__chips { align-items: center; flex-wrap: wrap; justify-content: flex-end; }
3298
- .td-day__repo { font: inherit; font-size: 12px; color: var(--foreground); background: var(--background); border: 1px solid var(--border); border-radius: 0; padding: 2px 6px; max-width: 160px; }
3305
+ .td-day__repo { font: inherit; font-size: 12px; color: var(--foreground); background: var(--background); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; max-width: 160px; }
3299
3306
  .td-day__repo:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
3300
3307
  .td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
3301
3308
  .td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
@@ -3306,7 +3313,7 @@
3306
3313
 
3307
3314
  /* the detail */
3308
3315
  .td-day__dhead { display: grid; gap: 4px; }
3309
- .td-day__kind { font: 11px var(--mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground); }
3316
+ .td-day__kind { font: 11px var(--mono); letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-foreground); }
3310
3317
  .td-day__dhead h3 { margin: 0; font-size: 16px; line-height: 24px; color: var(--foreground); }
3311
3318
  .td-day__detail p { margin: 0; font-size: 14px; line-height: 20px; color: var(--foreground); }
3312
3319
  .td-day__detail p.td-day__note { font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
@@ -3317,11 +3324,10 @@
3317
3324
  height: 28px;
3318
3325
  padding: 0 10px;
3319
3326
  border: 1px solid var(--border);
3327
+ border-radius: 6px;
3320
3328
  color: var(--foreground);
3321
3329
  font-size: 12px;
3322
- font-weight: 600;
3323
- letter-spacing: 0.1em;
3324
- text-transform: uppercase;
3330
+ font-weight: 500;
3325
3331
  text-decoration: none;
3326
3332
  white-space: nowrap;
3327
3333
  }
@@ -3329,7 +3335,7 @@
3329
3335
  @media (pointer: coarse) { .td-day__a { min-height: 44px; } }
3330
3336
  .td-day__phase, .td-day__rel { display: grid; gap: 4px; }
3331
3337
  .td-day__phase h4, .td-day__rel h4, .td-day__notes h4, .td-day__draftbox h4, .td-day__more h4 {
3332
- margin: 0; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground);
3338
+ margin: 0; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-foreground);
3333
3339
  }
3334
3340
  .td-day__chk { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 14px; color: var(--foreground); }
3335
3341
  .td-day__chk li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; }
@@ -3351,7 +3357,7 @@
3351
3357
  .td-day__draft { margin: 0; max-height: 220px; overflow: auto; padding: 8px 10px; white-space: pre-wrap; font: 12px/1.5 var(--mono); background: var(--muted); color: var(--foreground); }
3352
3358
  .td-day__revise { display: grid; gap: 6px; }
3353
3359
  /* act and prepare (initiative today-act-prepare): a verb first, one sentence, the steps, one button that fits, the marks */
3354
- .td-day__verb { display: inline-block; min-width: 58px; padding: 2px 5px; border: 1px solid currentColor; font: 600 10px/1.3 var(--mono); letter-spacing: 0.05em; text-transform: uppercase; text-align: center; color: var(--foreground); white-space: nowrap; }
3360
+ .td-day__verb { display: inline-block; min-width: 58px; padding: 2px 5px; border: 1px solid currentColor; font: 600 10px/1.3 var(--mono); letter-spacing: 0.04em; text-transform: uppercase; text-align: center; color: var(--foreground); white-space: nowrap; }
3355
3361
  .td-day__verb[data-verb="decide"], .td-day__verb[data-verb="prepare"] { color: var(--warning); }
3356
3362
  .td-day__acth { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
3357
3363
  .td-day__todo { margin: 0; padding: 7px 10px; border-left: 3px solid var(--foreground); background: var(--muted); font-size: 14px; line-height: 1.45; color: var(--foreground); }
@@ -3459,7 +3465,7 @@
3459
3465
  gap: 8px;
3460
3466
  min-width: 0;
3461
3467
  padding: 10px 12px;
3462
- border-radius: 0;
3468
+ border-radius: 8px;
3463
3469
  background: var(--card);
3464
3470
  color: var(--foreground);
3465
3471
  box-shadow: 0 0 0 1px var(--border);
@@ -3553,7 +3559,7 @@
3553
3559
  }
3554
3560
  .td-health {
3555
3561
  padding: 8px 12px;
3556
- border-radius: 0;
3562
+ border-radius: 8px;
3557
3563
  background: var(--muted);
3558
3564
  box-shadow: inset 2px 0 0 var(--warning);
3559
3565
  font-size: 12px;
@@ -4011,13 +4017,13 @@
4011
4017
  margin: 16px 16px 0;
4012
4018
  padding: 12px;
4013
4019
  border: 1px solid var(--border);
4014
- border-radius: 0;
4020
+ border-radius: 8px;
4015
4021
  }
4016
4022
  .bo-releases__head {
4017
4023
  font-size: 12px;
4018
4024
  line-height: 16px;
4019
4025
  font-weight: 600;
4020
- letter-spacing: 0.06em;
4026
+ letter-spacing: 0.04em;
4021
4027
  text-transform: uppercase;
4022
4028
  }
4023
4029
  .bo-releases__note {
@@ -4036,7 +4042,7 @@
4036
4042
  .bo-rel--today {
4037
4043
  padding: 10px 12px;
4038
4044
  border: 1px solid var(--border);
4039
- border-radius: 0;
4045
+ border-radius: 8px;
4040
4046
  background: var(--card);
4041
4047
  }
4042
4048
  .bo-rel__main {
@@ -4119,7 +4125,7 @@
4119
4125
  font-size: 11px;
4120
4126
  line-height: 16px;
4121
4127
  font-weight: 500;
4122
- letter-spacing: 0.1em;
4128
+ letter-spacing: 0.04em;
4123
4129
  text-transform: uppercase;
4124
4130
  color: var(--muted-foreground);
4125
4131
  }
@@ -4266,7 +4272,7 @@
4266
4272
  font-family: var(--mono);
4267
4273
  font-size: 11px;
4268
4274
  font-weight: 500;
4269
- letter-spacing: 0.1em;
4275
+ letter-spacing: 0.04em;
4270
4276
  text-transform: uppercase;
4271
4277
  color: var(--muted-foreground);
4272
4278
  }
@@ -5883,6 +5889,8 @@
5883
5889
  }
5884
5890
  .nt-sr {
5885
5891
  position: absolute;
5892
+ top: 0;
5893
+ left: 0;
5886
5894
  width: 1px;
5887
5895
  height: 1px;
5888
5896
  overflow: hidden;
@@ -5925,7 +5933,7 @@
5925
5933
  margin: 0;
5926
5934
  font-size: 11px;
5927
5935
  font-weight: 500;
5928
- letter-spacing: .06em;
5936
+ letter-spacing: 0.04em;
5929
5937
  text-transform: uppercase;
5930
5938
  color: var(--muted-foreground);
5931
5939
  }
@@ -6200,7 +6208,7 @@
6200
6208
  gap: 2px;
6201
6209
  padding: 8px 12px;
6202
6210
  border: 1px solid var(--border);
6203
- border-radius: 0;
6211
+ border-radius: 8px;
6204
6212
  background: var(--background);
6205
6213
  color: var(--foreground);
6206
6214
  font: inherit;
@@ -6585,10 +6593,9 @@
6585
6593
  color: var(--foreground);
6586
6594
  }
6587
6595
 
6588
- /* R27: the dock draws no rounded corner. The tab strip and the scratchpad were drawn with inline radii (workspace-cockpit-nav);
6589
- this rule wins over them, so the strip is sharp like every other surface. A tab's colour dot is a dot and keeps its circle. */
6590
- .lg-dock *:not([data-tab-dot]) {
6591
- border-radius: 0 !important;
6592
- }
6596
+ /* R27 as amended by the PM 2026-10-07: the dock follows the same radii as every other surface (6px controls, 8px
6597
+ containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
6598
+ (8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
6599
+
6593
6600
 
6594
6601
  /* -- end of the layout sections: add nothing below this line -- */
@@ -12,7 +12,7 @@
12
12
  * The legacy Today view (view-today.js) calls mountToday on its own host, so both surfaces draw the one list.
13
13
  */
14
14
 
15
- import { el } from './dom.js';
15
+ import { el, withAnchor } from './dom.js';
16
16
  import { empty, tabs, button, toast, ensureComponentsCss } from './primitives.js';
17
17
  import { registerTodaySource, todayCards, todayCount, openInitiative } from './registry.js';
18
18
  import { repoTuples } from './panel-repos.js';
@@ -200,7 +200,8 @@ export function mountToday(host, ctx) {
200
200
  if (out.sig === r.sig) return;
201
201
  const k = focusKey(r.root);
202
202
  r.sig = out.sig;
203
- r.root.replaceChildren(...out.children.filter(Boolean));
203
+ // a card added or removed above the reader must not move what they are reading (dom.js withAnchor)
204
+ withAnchor(host, r.root, 'data-today-id', () => r.root.replaceChildren(...out.children.filter(Boolean)));
204
205
  refocus(r.root, k);
205
206
  }
206
207
 
@@ -114,3 +114,63 @@ export function setChildren(parent, kids) {
114
114
  });
115
115
  return parent;
116
116
  }
117
+
118
+ /**
119
+ * Keep the reader's place across a rebuild (UI comfort, 2026-10-07: "when I scroll down into a section it pages up").
120
+ * Before `rebuild()` runs, the first element carrying `attr` that is still on screen in `scroller` is remembered with its
121
+ * offset from the scroller's top; afterwards the scroller is moved so that same element sits at that same offset. A
122
+ * card added or removed above the reader therefore no longer pushes what they are reading off the screen. With the
123
+ * scroller at its top, or no such element, it only runs `rebuild()`.
124
+ */
125
+ export function withAnchor(scroller, root, attr, rebuild) {
126
+ let key = null;
127
+ let off = 0;
128
+ if (scroller && scroller.scrollTop > 0 && root) {
129
+ const top = scroller.getBoundingClientRect().top;
130
+ for (const n of root.querySelectorAll('[' + attr + ']')) {
131
+ const r = n.getBoundingClientRect();
132
+ if (r.height && r.bottom > top) { key = n.getAttribute(attr); off = r.top - top; break; }
133
+ }
134
+ }
135
+ rebuild();
136
+ if (key == null) return;
137
+ const again = [...root.querySelectorAll('[' + attr + ']')].find((n) => n.getAttribute(attr) === key);
138
+ if (!again) return;
139
+ const drift = again.getBoundingClientRect().top - scroller.getBoundingClientRect().top - off;
140
+ if (Math.abs(drift) >= 1) scroller.scrollTop += drift;
141
+ }
142
+
143
+ /**
144
+ * Give a scroller its reading position back when a rebuild took it away (UI comfort, 2026-10-07). A destination that
145
+ * redraws through a short state — a "Reading…" line, an emptied list — collapses the scroller, the browser clamps the
146
+ * offset towards the top, and when the content returns the reader is left at the top. This watches the scroller's
147
+ * subtree: when a change leaves the offset ABOVE where the reader last was and the content has shrunk below it, that
148
+ * place is held, and the next change that makes the content tall enough again returns to it. Anything the reader does
149
+ * (wheel, touch, a key, a click) drops what is held, so their own scrolling always wins. Returns its undo.
150
+ */
151
+ export function keepScroll(scroller) {
152
+ let top = scroller.scrollTop;
153
+ let held = null;
154
+ let until = 0;
155
+ const drop = () => { held = null; };
156
+ const onScroll = () => { if (held == null) top = scroller.scrollTop; };
157
+ const intents = ['wheel', 'touchstart', 'keydown', 'pointerdown'];
158
+ for (const ev of intents) scroller.addEventListener(ev, drop, { passive: true, capture: true });
159
+ scroller.addEventListener('scroll', onScroll, { passive: true });
160
+ const mo = new MutationObserver(() => {
161
+ if (scroller.hidden || !scroller.isConnected) return;
162
+ const max = scroller.scrollHeight - scroller.clientHeight;
163
+ if (held == null) {
164
+ if (top > 0 && scroller.scrollTop < top - 1 && max < top) { held = top; until = Date.now() + 5000; }
165
+ return;
166
+ }
167
+ if (Date.now() > until) { held = null; top = scroller.scrollTop; return; }
168
+ if (max >= held - 1) { scroller.scrollTop = held; top = held; held = null; }
169
+ });
170
+ mo.observe(scroller, { childList: true, subtree: true, characterData: true });
171
+ return () => {
172
+ mo.disconnect();
173
+ for (const ev of intents) scroller.removeEventListener(ev, drop, { capture: true });
174
+ scroller.removeEventListener('scroll', onScroll);
175
+ };
176
+ }
@@ -20,7 +20,7 @@
20
20
  * env = { repo, ws, openInitiative(repo, slug), onData?(answer) }
21
21
  */
22
22
 
23
- import { el } from './dom.js';
23
+ import { el, withAnchor } from './dom.js';
24
24
  import { card, iconButton, dragHandle, button, menu, empty, toast, SEL } from './primitives.js';
25
25
  import { watchSig } from './sig-watch.js';
26
26
  import { loadSortable, baseOptions, dragSet } from './bd-drag.js';
@@ -211,7 +211,7 @@ export function mountRoadmap(host, env) {
211
211
  const kids = [headLine(), el('p', { class: 'in-note in-hint', 'data-in-hint': '' }, COPY.menuHintCard)];
212
212
  if (data.flags && data.flags.length) kids.push(el('p', { class: 'in-note', 'data-in-flags': '' }, COPY.flags(data.flags.length)));
213
213
  kids.push(el('div', { class: 'in-cols', 'data-in-cols': '' }, HORIZONS.map(column)));
214
- root.replaceChildren(...kids);
214
+ withAnchor(root.closest('.fr-dest'), root, 'data-in-card', () => root.replaceChildren(...kids));
215
215
  if (refocus) { const t = root.querySelector('[data-in-open="' + CSS.escape(refocus) + '"]'); if (t) t.focus({ preventScroll: true }); }
216
216
  bindDrag();
217
217
  }
@@ -36,6 +36,9 @@ export function unsyncedOf(st) {
36
36
  && ((st.behind | 0) > 0 || (st.ahead | 0) > 0 || (st.dirty_records | 0) > 0 || st.remote_stale === true);
37
37
  }
38
38
 
39
+ /** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
40
+ const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'share-2', settings: 'settings' });
41
+
39
42
  export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
40
43
  const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
41
44
 
@@ -48,7 +51,7 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
48
51
  const links = {};
49
52
  for (const id of [...BAR_DESTS, 'settings']) {
50
53
  links[id] = button({
51
- label: COPY.nav[id], variant: 'ghost', icon: id === 'settings' ? 'settings' : undefined,
54
+ label: COPY.nav[id], variant: 'ghost', icon: NAV_ICON[id],
52
55
  attrs: { class: 'fr-link', 'data-fr-dest': id, 'aria-current': 'false' },
53
56
  onClick: () => go && go(id),
54
57
  });
@@ -18,10 +18,10 @@
18
18
  * file in exactly one shard), and --check fails when a suite on disk is in no list, so a new suite
19
19
  * that nobody wired in is a red build rather than a suite that runs nowhere.
20
20
  *
21
- * --changed is for the LOCAL inner loop only. It narrows the group to the suites a diff against REF
22
- * (default origin/main) can reach, and runs the whole group whenever it cannot tell. CI never passes
23
- * it: Workspace CI runs every suite on every PR, so a suite --changed leaves out still runs before
24
- * the merge. See changedSuites() for the rule.
21
+ * --changed narrows the group to the suites a diff against REF (default origin/main) can reach, and
22
+ * runs the whole group whenever it cannot tell. The local inner loop uses it, and so does Workspace
23
+ * CI for the browser group on a PR (ci-browser-after-merge): every browser suite still runs after the
24
+ * merge on main, and kit-publish waits for that run. See changedSuites() for the rule.
25
25
  *
26
26
  * Output: a suite's own output is buffered and printed whole when it finishes, so lines from two
27
27
  * suites never interleave. A passing suite prints its last lines; --verbose prints all of it.
@@ -256,7 +256,7 @@ async function main() {
256
256
  if (changed) {
257
257
  const ref = changed === true ? 'origin/main' : changed;
258
258
  const pick = changedSuites(list, ref);
259
- console.log(`run-tests: --changed against ${ref}: ${pick.why}. ${pick.suites.length} of ${list.length} suites run here; CI runs all ${list.length}.`);
259
+ console.log(`run-tests: --changed against ${ref}: ${pick.why}. ${pick.suites.length} of ${list.length} suites run here; every suite runs on main after the merge.`);
260
260
  list = pick.suites;
261
261
  if (!list.length) return;
262
262
  }