create-pmos 0.80.0 → 0.81.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.81.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@3a0843a
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-07T14:50:35Z"}
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-07T14:50:35Z"}
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-07T14:50:35Z"}
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.
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.81.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.81.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.81.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;
@@ -615,7 +618,7 @@
615
618
  overflow: auto;
616
619
  padding: 4px;
617
620
  border: 0;
618
- border-radius: 0;
621
+ border-radius: 8px;
619
622
  background: var(--card);
620
623
  color: var(--foreground);
621
624
  font-family: var(--sans);
@@ -633,7 +636,7 @@
633
636
  min-height: 32px;
634
637
  padding: 0 12px;
635
638
  border: 0;
636
- border-radius: 0;
639
+ border-radius: 6px;
637
640
  background: transparent;
638
641
  color: var(--foreground);
639
642
  font: inherit;
@@ -663,7 +666,7 @@
663
666
  margin: 0;
664
667
  padding: 0;
665
668
  border: 0;
666
- border-radius: 0;
669
+ border-radius: 8px;
667
670
  background: var(--card);
668
671
  color: var(--foreground);
669
672
  font-family: var(--sans);
@@ -794,7 +797,7 @@
794
797
  min-height: 44px;
795
798
  padding: 4px 4px 4px 16px;
796
799
  border: 0;
797
- border-radius: 0;
800
+ border-radius: 8px;
798
801
  background: var(--card);
799
802
  color: var(--foreground);
800
803
  font-family: var(--sans);
@@ -936,7 +939,7 @@
936
939
  margin: 0;
937
940
  padding: 0;
938
941
  border: 0;
939
- border-radius: 0;
942
+ border-radius: 6px;
940
943
  background: transparent;
941
944
  color: inherit;
942
945
  transform: translateY(-50%);
@@ -1084,7 +1087,7 @@
1084
1087
  margin-right: 10px;
1085
1088
  font-size: 12px;
1086
1089
  font-weight: 600;
1087
- letter-spacing: 0.08em;
1090
+ letter-spacing: 0.04em;
1088
1091
  text-transform: uppercase;
1089
1092
  }
1090
1093
  .bd-entry__meta {
@@ -1200,7 +1203,7 @@
1200
1203
  font-size: 11px;
1201
1204
  line-height: 16px;
1202
1205
  font-weight: 600;
1203
- letter-spacing: 0.05em;
1206
+ letter-spacing: 0.04em;
1204
1207
  text-transform: uppercase;
1205
1208
  color: var(--muted-foreground);
1206
1209
  }
@@ -1287,7 +1290,7 @@
1287
1290
  font-size: 12px;
1288
1291
  line-height: 16px;
1289
1292
  font-weight: 600;
1290
- letter-spacing: 0.06em;
1293
+ letter-spacing: 0.04em;
1291
1294
  text-transform: uppercase;
1292
1295
  }
1293
1296
  .bd-col__n {
@@ -1545,10 +1548,9 @@
1545
1548
  .in-head__title {
1546
1549
  flex: 1 1 auto;
1547
1550
  margin: 0;
1548
- font-size: 16px;
1549
- line-height: 24px;
1550
- font-weight: 600;
1551
- letter-spacing: -0.01em;
1551
+ font-size: 20px;
1552
+ line-height: 28px;
1553
+ font-weight: 500;
1552
1554
  }
1553
1555
  .in-body {
1554
1556
  display: flex;
@@ -1645,7 +1647,7 @@
1645
1647
  font-size: 12px;
1646
1648
  line-height: 16px;
1647
1649
  font-weight: 600;
1648
- letter-spacing: 0.06em;
1650
+ letter-spacing: 0.04em;
1649
1651
  text-transform: uppercase;
1650
1652
  font-variant-numeric: tabular-nums;
1651
1653
  }
@@ -1670,7 +1672,7 @@
1670
1672
  font-size: 12px;
1671
1673
  line-height: 16px;
1672
1674
  font-weight: 600;
1673
- letter-spacing: 0.06em;
1675
+ letter-spacing: 0.04em;
1674
1676
  text-transform: uppercase;
1675
1677
  }
1676
1678
  .in-col__count {
@@ -1910,7 +1912,7 @@
1910
1912
  font-size: 12px;
1911
1913
  line-height: 16px;
1912
1914
  font-weight: 600;
1913
- letter-spacing: 0.06em;
1915
+ letter-spacing: 0.04em;
1914
1916
  text-transform: uppercase;
1915
1917
  }
1916
1918
  .in-starters {
@@ -2133,7 +2135,7 @@
2133
2135
  margin: 0;
2134
2136
  font-size: 12px;
2135
2137
  font-weight: 600;
2136
- letter-spacing: 0.1em;
2138
+ letter-spacing: 0.04em;
2137
2139
  text-transform: uppercase;
2138
2140
  color: var(--muted-foreground);
2139
2141
  }
@@ -2266,6 +2268,8 @@
2266
2268
  min-width: 0;
2267
2269
  min-height: 0;
2268
2270
  overflow-y: auto;
2271
+ /* the destination is the one thing that scrolls: a wheel at its end does not move the page and its bars */
2272
+ overscroll-behavior: contain;
2269
2273
  }
2270
2274
  .fr-dest[hidden] {
2271
2275
  display: none;
@@ -2476,7 +2480,7 @@
2476
2480
  color: var(--muted-foreground);
2477
2481
  font-size: 11px;
2478
2482
  font-weight: 600;
2479
- letter-spacing: 0.1em;
2483
+ letter-spacing: 0.04em;
2480
2484
  text-transform: uppercase;
2481
2485
  text-overflow: ellipsis;
2482
2486
  white-space: nowrap;
@@ -2765,7 +2769,7 @@
2765
2769
  color: var(--muted-foreground);
2766
2770
  font-size: 11px;
2767
2771
  font-weight: 600;
2768
- letter-spacing: 0.1em;
2772
+ letter-spacing: 0.04em;
2769
2773
  text-transform: uppercase;
2770
2774
  list-style: none;
2771
2775
  cursor: pointer;
@@ -3007,7 +3011,7 @@
3007
3011
  color: var(--muted-foreground);
3008
3012
  font-size: 11px;
3009
3013
  font-weight: 600;
3010
- letter-spacing: 0.1em;
3014
+ letter-spacing: 0.04em;
3011
3015
  text-transform: uppercase;
3012
3016
  }
3013
3017
  .fr-rail__graph {
@@ -3089,7 +3093,7 @@
3089
3093
  color: var(--muted-foreground);
3090
3094
  font-size: 11px;
3091
3095
  font-weight: 600;
3092
- letter-spacing: 0.1em;
3096
+ letter-spacing: 0.04em;
3093
3097
  text-transform: uppercase;
3094
3098
  }
3095
3099
  .fr-rail__finding[data-status="stale"] .fr-rail__finding-s,
@@ -3245,7 +3249,7 @@
3245
3249
  .td-day__warn { color: var(--warning); }
3246
3250
  .td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
3247
3251
  .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); }
3252
+ .td-day__ts { border-radius: 6px; gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
3249
3253
  .td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
3250
3254
  .td-day__ts[data-step-state="now"] { background: var(--muted); }
3251
3255
  .td-day__ts[aria-current="true"] { border-color: var(--ring); }
@@ -3260,14 +3264,15 @@
3260
3264
  .td-day__run--failed { color: var(--destructive); }
3261
3265
  .td-day__note { margin: 0; font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
3262
3266
 
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; }
3267
+ .td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: stretch; }
3268
+ .td-day__left { display: grid; gap: 12px; min-width: 0; align-content: start; }
3265
3269
  .td-day__panel, .td-day__detail {
3266
3270
  display: grid;
3267
3271
  gap: 8px;
3268
3272
  min-width: 0;
3269
3273
  padding: 10px 12px;
3270
3274
  border: 1px solid var(--border);
3275
+ border-radius: 8px;
3271
3276
  background: var(--card);
3272
3277
  }
3273
3278
  .td-day__detail { position: sticky; top: 8px; max-height: calc(100vh - 16px); overflow: auto; align-content: start; }
@@ -3282,7 +3287,7 @@
3282
3287
  margin: 0;
3283
3288
  font-size: 12px;
3284
3289
  font-weight: 600;
3285
- letter-spacing: 0.06em;
3290
+ letter-spacing: 0.04em;
3286
3291
  text-transform: uppercase;
3287
3292
  color: var(--muted-foreground);
3288
3293
  }
@@ -3295,7 +3300,7 @@
3295
3300
  .td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
3296
3301
  .td-day__chips { display: inline-flex; gap: 4px; }
3297
3302
  .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; }
3303
+ .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
3304
  .td-day__repo:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
3300
3305
  .td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
3301
3306
  .td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
@@ -3306,7 +3311,7 @@
3306
3311
 
3307
3312
  /* the detail */
3308
3313
  .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); }
3314
+ .td-day__kind { font: 11px var(--mono); letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-foreground); }
3310
3315
  .td-day__dhead h3 { margin: 0; font-size: 16px; line-height: 24px; color: var(--foreground); }
3311
3316
  .td-day__detail p { margin: 0; font-size: 14px; line-height: 20px; color: var(--foreground); }
3312
3317
  .td-day__detail p.td-day__note { font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
@@ -3317,11 +3322,10 @@
3317
3322
  height: 28px;
3318
3323
  padding: 0 10px;
3319
3324
  border: 1px solid var(--border);
3325
+ border-radius: 6px;
3320
3326
  color: var(--foreground);
3321
3327
  font-size: 12px;
3322
- font-weight: 600;
3323
- letter-spacing: 0.1em;
3324
- text-transform: uppercase;
3328
+ font-weight: 500;
3325
3329
  text-decoration: none;
3326
3330
  white-space: nowrap;
3327
3331
  }
@@ -3329,7 +3333,7 @@
3329
3333
  @media (pointer: coarse) { .td-day__a { min-height: 44px; } }
3330
3334
  .td-day__phase, .td-day__rel { display: grid; gap: 4px; }
3331
3335
  .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);
3336
+ margin: 0; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-foreground);
3333
3337
  }
3334
3338
  .td-day__chk { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 14px; color: var(--foreground); }
3335
3339
  .td-day__chk li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; }
@@ -3351,7 +3355,7 @@
3351
3355
  .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
3356
  .td-day__revise { display: grid; gap: 6px; }
3353
3357
  /* 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; }
3358
+ .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
3359
  .td-day__verb[data-verb="decide"], .td-day__verb[data-verb="prepare"] { color: var(--warning); }
3356
3360
  .td-day__acth { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
3357
3361
  .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 +3463,7 @@
3459
3463
  gap: 8px;
3460
3464
  min-width: 0;
3461
3465
  padding: 10px 12px;
3462
- border-radius: 0;
3466
+ border-radius: 8px;
3463
3467
  background: var(--card);
3464
3468
  color: var(--foreground);
3465
3469
  box-shadow: 0 0 0 1px var(--border);
@@ -3553,7 +3557,7 @@
3553
3557
  }
3554
3558
  .td-health {
3555
3559
  padding: 8px 12px;
3556
- border-radius: 0;
3560
+ border-radius: 8px;
3557
3561
  background: var(--muted);
3558
3562
  box-shadow: inset 2px 0 0 var(--warning);
3559
3563
  font-size: 12px;
@@ -4011,13 +4015,13 @@
4011
4015
  margin: 16px 16px 0;
4012
4016
  padding: 12px;
4013
4017
  border: 1px solid var(--border);
4014
- border-radius: 0;
4018
+ border-radius: 8px;
4015
4019
  }
4016
4020
  .bo-releases__head {
4017
4021
  font-size: 12px;
4018
4022
  line-height: 16px;
4019
4023
  font-weight: 600;
4020
- letter-spacing: 0.06em;
4024
+ letter-spacing: 0.04em;
4021
4025
  text-transform: uppercase;
4022
4026
  }
4023
4027
  .bo-releases__note {
@@ -4036,7 +4040,7 @@
4036
4040
  .bo-rel--today {
4037
4041
  padding: 10px 12px;
4038
4042
  border: 1px solid var(--border);
4039
- border-radius: 0;
4043
+ border-radius: 8px;
4040
4044
  background: var(--card);
4041
4045
  }
4042
4046
  .bo-rel__main {
@@ -4119,7 +4123,7 @@
4119
4123
  font-size: 11px;
4120
4124
  line-height: 16px;
4121
4125
  font-weight: 500;
4122
- letter-spacing: 0.1em;
4126
+ letter-spacing: 0.04em;
4123
4127
  text-transform: uppercase;
4124
4128
  color: var(--muted-foreground);
4125
4129
  }
@@ -4266,7 +4270,7 @@
4266
4270
  font-family: var(--mono);
4267
4271
  font-size: 11px;
4268
4272
  font-weight: 500;
4269
- letter-spacing: 0.1em;
4273
+ letter-spacing: 0.04em;
4270
4274
  text-transform: uppercase;
4271
4275
  color: var(--muted-foreground);
4272
4276
  }
@@ -5883,6 +5887,8 @@
5883
5887
  }
5884
5888
  .nt-sr {
5885
5889
  position: absolute;
5890
+ top: 0;
5891
+ left: 0;
5886
5892
  width: 1px;
5887
5893
  height: 1px;
5888
5894
  overflow: hidden;
@@ -5925,7 +5931,7 @@
5925
5931
  margin: 0;
5926
5932
  font-size: 11px;
5927
5933
  font-weight: 500;
5928
- letter-spacing: .06em;
5934
+ letter-spacing: 0.04em;
5929
5935
  text-transform: uppercase;
5930
5936
  color: var(--muted-foreground);
5931
5937
  }
@@ -6200,7 +6206,7 @@
6200
6206
  gap: 2px;
6201
6207
  padding: 8px 12px;
6202
6208
  border: 1px solid var(--border);
6203
- border-radius: 0;
6209
+ border-radius: 8px;
6204
6210
  background: var(--background);
6205
6211
  color: var(--foreground);
6206
6212
  font: inherit;
@@ -6585,10 +6591,9 @@
6585
6591
  color: var(--foreground);
6586
6592
  }
6587
6593
 
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
- }
6594
+ /* R27 as amended by the PM 2026-10-07: the dock follows the same radii as every other surface (6px controls, 8px
6595
+ containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
6596
+ (8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
6597
+
6593
6598
 
6594
6599
  /* -- 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
  }