great-cto 3.2.0 → 3.3.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/README.md CHANGED
@@ -20,6 +20,25 @@ That's it. The CLI detects your stack, picks the right archetype, clones the plu
20
20
 
21
21
  After install, restart Claude Code and run `/inbox` or `/audit`.
22
22
 
23
+ ## What the plugin gives you
24
+
25
+ The CLI installs it; these are what it installs.
26
+
27
+ - **A pipeline of specialist agents** — architect, product owner, senior dev, QA,
28
+ security officer, and archetype-specific reviewers — that run in sequence with
29
+ human approval gates at the points that are expensive to undo.
30
+ - **Spending caps that refuse.** Set a per-agent cap; when measured spend passes
31
+ it the dispatcher declines to run that agent instead of quietly continuing. An
32
+ agent with no measurement reads as `unmeasured`, never as `$0`.
33
+ - **A board at `localhost:3141`** — tasks, metrics, per-agent budgets, docs
34
+ search, and the decision journal, all served from your own machine. Nothing
35
+ leaves it.
36
+ - **A decision journal** that records what each dispatch decided and why, so a
37
+ pipeline that did not run looks different from one that ran and found nothing.
38
+
39
+ Telemetry is opt-in and off by default — see
40
+ [docs/PRIVACY.md](https://github.com/avelikiy/great_cto/blob/main/docs/PRIVACY.md).
41
+
23
42
  ## Examples
24
43
 
25
44
  ### Commerce detection (Stripe + Next.js)
@@ -82,8 +101,10 @@ npx great-cto init [options]
82
101
  ## Trust signals
83
102
 
84
103
  - Zero runtime dependencies — only Node built-ins (`node:fs`, `node:path`, etc.)
85
- - 47 unit tests covering stack detection, archetype scoring, settings merge
86
- - CI matrix: Node 18/20/22 × Ubuntu/macOS/Windows
104
+ - 305 unit tests covering stack detection, archetype scoring, and settings merge
105
+ - Tests and the release gate run locally (`scripts/ci-local.sh`) the hosted CI
106
+ workflows are present in the repo but have not run since June 2026, so treat the
107
+ badge-shaped claims in this file as claims about the local gate, not a cloud matrix
87
108
  - Source: [`packages/cli/src/`](https://github.com/avelikiy/great_cto/tree/main/packages/cli/src) — ~1.2k LOC TypeScript
88
109
 
89
110
  ## License
@@ -2,7 +2,7 @@
2
2
  "name": "great_cto",
3
3
  "id": "great_cto",
4
4
  "description": "Engineering process for solo founders and teams up to 50 engineers. Agents do architecture, code review, QA, and security. You make two decisions per feature.",
5
- "version": "3.2.0",
5
+ "version": "3.3.0",
6
6
  "author": {
7
7
  "name": "Great CTO",
8
8
  "url": "https://github.com/avelikiy/great_cto"
@@ -1174,9 +1174,24 @@ async function dispatch(req, res, url, cwd) {
1174
1174
  return (x[0] - y[0]) || (x[1] - y[1]) || (x[2] - y[2]);
1175
1175
  }).pop() || null;
1176
1176
  } catch { installed = null; }
1177
- const stale = installed && BUILD_VERSION !== 'unknown'
1178
- ? (installed !== BUILD_VERSION ? 'yes' : 'no')
1179
- : 'unknown';
1177
+ // Direction, not inequality. `installed !== BUILD_VERSION` is true in both
1178
+ // directions, and the banner it fed said "restart to pick up the new
1179
+ // version" either way — so a board running 3.2.0 against an installed 3.1.0
1180
+ // was told to restart into the OLDER build. A prompt to downgrade that
1181
+ // looks exactly like a prompt to upgrade is the same defect this endpoint
1182
+ // was written to fix, pointed the other way.
1183
+ //
1184
+ // Four states: `behind` (an install is waiting), `ahead` (the running code
1185
+ // is newer — restarting would lose it), `no`, and `unknown`.
1186
+ const cmp = (a, b) => {
1187
+ const x = a.split('.').map(Number), y = b.split('.').map(Number);
1188
+ return (x[0] - y[0]) || (x[1] - y[1]) || (x[2] - y[2]);
1189
+ };
1190
+ let stale = 'unknown';
1191
+ if (installed && BUILD_VERSION !== 'unknown' && /^\d+\.\d+\.\d+$/.test(BUILD_VERSION)) {
1192
+ const d = cmp(installed, BUILD_VERSION);
1193
+ stale = d > 0 ? 'behind' : d < 0 ? 'ahead' : 'no';
1194
+ }
1180
1195
  res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
1181
1196
  res.end(JSON.stringify({
1182
1197
  version: BUILD_VERSION, installed, stale,
@@ -123,6 +123,38 @@
123
123
  --shadow-card-hover: 0 1px 3px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(0, 168, 98, 0.08);
124
124
  --drawer-shadow: 0 16px 32px rgba(0, 0, 0, 0.18);
125
125
  }
126
+ /* ── Type scale ──────────────────────────────────────────────────────────────
127
+ The board had 22 raw font sizes (9px…52px, including 11.5/12.5/13.5) and no
128
+ rule for h1/h2/h3 at all — so Budgets, Docs, Sessions and Activity rendered
129
+ their titles in the browser's default bold 700, a weight used nowhere else in
130
+ the design. One role, six renderings.
131
+
132
+ Two ramps, because the board has two kinds of text. Text steps by +1px in the
133
+ dense band (this is a single-operator console; density is the point) and by
134
+ ~1.2x in the title band. Live numbers get their own ramp: they are read as
135
+ quantities, not prose, so they take --serif at 500 with tabular-nums.
136
+
137
+ --serif is NOT a serif face — it resolves to the same Geist as --sans. The
138
+ name is legacy; the role is "display numerals". Reach for it only at >=24px.
139
+
140
+ Legal 10px: --fs-eyebrow, and only for mono uppercase labels with tracking.
141
+ 10px in sans body text is not a size, it is a mistake. */
142
+ :root {
143
+ --fs-eyebrow: 10px; /* mono uppercase label, tracking 0.08–0.1em */
144
+ --fs-caption: 11px; /* metadata, counts, timestamps */
145
+ --fs-small: 12px; /* table cells, secondary meta, chips */
146
+ --fs-body: 13px; /* dense body: cards, list rows, form fields */
147
+ --fs-base: 14px; /* document base / longform prose */
148
+ --fs-title-s: 15px; /* card or section heading (h3 role) */
149
+ --fs-title-m: 18px; /* region heading (h2 role) */
150
+ --fs-title-l: 22px; /* page heading (h1 role) */
151
+
152
+ --fs-num-s: 24px; /* inline stat */
153
+ --fs-num-m: 30px; /* secondary metric card */
154
+ --fs-num-l: 36px; /* second-tier hero KPI */
155
+ --fs-num-xl: 52px; /* primary hero KPI */
156
+ }
157
+
126
158
  * { box-sizing: border-box; margin: 0; padding: 0; }
127
159
  html, body { height: 100%; }
128
160
  body {
@@ -135,6 +167,51 @@ body {
135
167
  }
136
168
  button { font-family: inherit; cursor: pointer; }
137
169
 
170
+ /* Headings had no base rule, so every unstyled h1/h2/h3 fell to the browser's
171
+ bold 700. Three weights exist in this design — 400 body, 500 value, 600 title
172
+ — and 700 is not one of them. This removes it everywhere at once. Bare-element
173
+ specificity (0,0,1), so every component rule below still wins. */
174
+ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
175
+
176
+ /* The page's own title. Present on every panel, one size, one weight. Before
177
+ this, two panels had no title element at all, four rendered the browser
178
+ default, and one rendered a section heading at title size. */
179
+ .page-title {
180
+ font-size: var(--fs-title-l);
181
+ font-weight: 600;
182
+ line-height: 1.25;
183
+ letter-spacing: -0.02em;
184
+ color: var(--text);
185
+ margin: 0 0 14px;
186
+ }
187
+
188
+ /* A heading INSIDE a page. Notifications rendered its three sections through
189
+ .share-card h2 — the showcase hero — so "Email alerts" was set at 28px, the
190
+ size of a page title, three times on one screen. */
191
+ .section-title {
192
+ font-size: var(--fs-title-m);
193
+ font-weight: 600;
194
+ line-height: 1.3;
195
+ letter-spacing: -0.01em;
196
+ color: var(--text);
197
+ margin: 0 0 6px;
198
+ }
199
+
200
+ /* Sessions had a .panel-head element with no rule of any kind: its h2 fell to
201
+ the browser default of 1.5em bold-700 and the search box beside it did not
202
+ align to anything. */
203
+ .panel-head {
204
+ display: flex; align-items: center; gap: 12px;
205
+ margin: 0 0 14px; flex-wrap: wrap;
206
+ }
207
+ .panel-head h2 {
208
+ font-size: var(--fs-title-l);
209
+ letter-spacing: -0.02em;
210
+ line-height: 1.25;
211
+ margin: 0; flex: 1 1 auto;
212
+ }
213
+
214
+
138
215
  .board {
139
216
  position: relative;
140
217
  width: 100%;
@@ -554,7 +631,7 @@ button { font-family: inherit; cursor: pointer; }
554
631
  flex-shrink: 0;
555
632
  }
556
633
  .card .title {
557
- font-weight: 600; font-size: 13px;
634
+ font-weight: 600; font-size: var(--fs-body);
558
635
  line-height: 1.35;
559
636
  color: var(--text);
560
637
  display: -webkit-box;
@@ -628,13 +705,13 @@ button { font-family: inherit; cursor: pointer; }
628
705
  #agent-panel.open { display: flex; }
629
706
  #agent-panel header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border, #1f2a26); background: transparent; }
630
707
  #agent-panel header b { font-size: 13px; }
631
- #agent-panel .ap-x { margin-left: auto; cursor: pointer; border: 0; background: none; color: var(--text-muted, #8aa); font-size: 16px; }
708
+ #agent-panel .ap-x { margin-left: auto; cursor: pointer; border: 0; background: none; color: var(--text3); font-size: 16px; }
632
709
  #agent-log { flex: 1; overflow: auto; padding: 10px 14px; font: 12px/1.55 ui-monospace, Menlo, monospace; }
633
710
  #agent-log .al { margin: 2px 0; white-space: pre-wrap; word-break: break-word; }
634
- #agent-log .al.tool { color: var(--status-review); } #agent-log .al.system { color: var(--text-muted, #8aa); } #agent-log .al.error { color: var(--status-blocked, #ff5a5a); } #agent-log .al.done { color: var(--text-muted, #8aa); font-weight: 600; }
711
+ #agent-log .al.tool { color: var(--status-review); } #agent-log .al.system { color: var(--text3); } #agent-log .al.error { color: var(--status-blocked, #ff5a5a); } #agent-log .al.done { color: var(--text3); font-weight: 600; }
635
712
  #agent-panel .ap-foot { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--border, #1f2a26); }
636
- #agent-panel input { flex: 1; background: var(--bg, #0a0e0c); color: var(--text, #ecf2ee); border: 1px solid var(--border, #1f2a26); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }
637
- #agent-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted, #556); }
713
+ #agent-panel input { flex: 1; background: var(--bg-muted); color: var(--text, #ecf2ee); border: 1px solid var(--border, #1f2a26); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }
714
+ #agent-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text3); }
638
715
  #agent-dot.live { background: var(--status-review); animation: apulse 1.2s infinite; }
639
716
  @keyframes apulse { 0%,100%{opacity:1} 50%{opacity:.4} }
640
717
  .gate-reject { color: var(--status-blocked); border-color: rgba(220, 38, 38, 0.30); }
@@ -677,7 +754,7 @@ button { font-family: inherit; cursor: pointer; }
677
754
  font-size: 12px;
678
755
  line-height: 1.45;
679
756
  }
680
- .degraded-banner strong { color: var(--text1); font-weight: 600; }
757
+ .degraded-banner strong { color: var(--text); font-weight: 600; }
681
758
  .degraded-banner span { color: var(--text2); font-family: var(--mono, ui-monospace, monospace); word-break: break-word; }
682
759
 
683
760
  /* ── Side panel ───────────────────────────────────────────────────────────── */
@@ -733,9 +810,9 @@ button { font-family: inherit; cursor: pointer; }
733
810
  .side-close:hover { background: var(--bg-muted); color: var(--text); }
734
811
  .side-body { flex: 1; overflow: auto; padding: 18px; }
735
812
  .side-title {
736
- font-family: var(--serif); font-weight: 600;
737
- font-size: 20px;
738
- letter-spacing: -0.02em;
813
+ font-family: var(--sans); font-weight: 600;
814
+ font-size: var(--fs-title-m);
815
+ letter-spacing: -0.01em;
739
816
  margin-bottom: 12px;
740
817
  color: var(--text);
741
818
  }
@@ -840,7 +917,7 @@ button { font-family: inherit; cursor: pointer; }
840
917
  text-transform: uppercase; color: var(--text3);
841
918
  min-width: 34px; flex-shrink: 0;
842
919
  }
843
- .cmdk-label { font-size: 13.5px; color: var(--text); flex: 1; min-width: 0;
920
+ .cmdk-label { font-size: var(--fs-body); color: var(--text); flex: 1; min-width: 0;
844
921
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
845
922
  .cmdk-hint { font-family: var(--mono); font-size: 11px; color: var(--text3); flex-shrink: 0; }
846
923
  .cmdk-empty { padding: 18px 14px; font-size: 13px; color: var(--text2); }
@@ -853,7 +930,7 @@ button { font-family: inherit; cursor: pointer; }
853
930
  position: absolute; inset: 0; background: rgba(0,0,0,0.5);
854
931
  }
855
932
  .hk-card {
856
- position: relative; background: var(--bg, #0f172a);
933
+ position: relative; background: var(--bg-elevated);
857
934
  color: var(--text); border: 1px solid var(--border);
858
935
  border-radius: 8px; padding: 20px 24px; min-width: 360px;
859
936
  box-shadow: 0 12px 32px rgba(0,0,0,0.5);
@@ -876,7 +953,7 @@ button { font-family: inherit; cursor: pointer; }
876
953
  }
877
954
  .rdm-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.5); }
878
955
  .rdm-card {
879
- position: relative; background: var(--bg, #0f172a);
956
+ position: relative; background: var(--bg-elevated);
880
957
  color: var(--text); border: 1px solid var(--border);
881
958
  border-radius: 8px; padding: 18px 22px; min-width: 480px; max-width: 720px;
882
959
  box-shadow: 0 12px 32px rgba(0,0,0,0.5);
@@ -903,10 +980,10 @@ button { font-family: inherit; cursor: pointer; }
903
980
  text-decoration: none; color: var(--text);
904
981
  font-family: ui-monospace, monospace;
905
982
  }
906
- .rdm-artefact:hover { background: var(--bg-hover, rgba(255,255,255,0.08)); }
983
+ .rdm-artefact:hover { background: var(--bg-muted); }
907
984
  .rdm-artefact .rdm-art-kind {
908
985
  font-size: 10px; padding: 1px 4px; border-radius: 3px;
909
- background: var(--accent-bg, rgba(59,130,246,0.2)); color: var(--accent, #3b82f6);
986
+ background: var(--accent-soft); color: var(--accent-text);
910
987
  }
911
988
  .rdm-foot { margin-top: 12px; font-size: 11px; color: var(--text3, #9ca3af); }
912
989
  .resume-item { cursor: pointer; }
@@ -937,9 +1014,9 @@ button { font-family: inherit; cursor: pointer; }
937
1014
  display: flex; align-items: center; justify-content: space-between;
938
1015
  padding: 14px 18px; border-bottom: 1px solid var(--border);
939
1016
  }
940
- .ni-header h3 { font-size: 14px; font-weight: 600; margin: 0; }
1017
+ .ni-header h3 { font-size: var(--fs-title-s); font-weight: 600; margin: 0; }
941
1018
  .ni-close {
942
- background: transparent; border: 0; color: var(--text-3, #9ca3af);
1019
+ background: transparent; border: 0; color: var(--text3);
943
1020
  cursor: pointer; padding: 4px; display: flex; border-radius: 4px;
944
1021
  }
945
1022
  .ni-close:hover { background: var(--bg-muted, rgba(255,255,255,0.06)); color: var(--text); }
@@ -952,8 +1029,8 @@ button { font-family: inherit; cursor: pointer; }
952
1029
  flex: 1;
953
1030
  }
954
1031
  .ni-label {
955
- font-size: 11px; color: var(--text-3, #9ca3af);
956
- text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500;
1032
+ font-family: var(--mono); font-size: var(--fs-eyebrow); color: var(--text3);
1033
+ text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500;
957
1034
  }
958
1035
  .ni-field input[type="text"],
959
1036
  .ni-field textarea,
@@ -981,7 +1058,7 @@ button { font-family: inherit; cursor: pointer; }
981
1058
  font-size: 13px; cursor: pointer;
982
1059
  }
983
1060
  .ni-checkbox input { margin: 0; cursor: pointer; }
984
- .ni-checkbox span { color: var(--text-2); }
1061
+ .ni-checkbox span { color: var(--text2); }
985
1062
  .ni-actions {
986
1063
  display: flex; justify-content: flex-end; gap: 8px;
987
1064
  margin-top: 4px;
@@ -992,7 +1069,7 @@ button { font-family: inherit; cursor: pointer; }
992
1069
  cursor: pointer; transition: all 120ms;
993
1070
  }
994
1071
  .ni-btn-ghost {
995
- background: transparent; color: var(--text-2);
1072
+ background: transparent; color: var(--text2);
996
1073
  border-color: var(--border);
997
1074
  }
998
1075
  .ni-btn-ghost:hover { background: var(--bg-muted, rgba(255,255,255,0.05)); color: var(--text); }
@@ -1024,7 +1101,7 @@ button { font-family: inherit; cursor: pointer; }
1024
1101
  color: var(--text3); margin-bottom: 6px;
1025
1102
  }
1026
1103
  .side-section-body {
1027
- font-size: 13.5px; line-height: 1.55;
1104
+ font-size: var(--fs-body); line-height: 1.5;
1028
1105
  color: var(--text2);
1029
1106
  white-space: pre-wrap;
1030
1107
  word-break: break-word;
@@ -1101,7 +1178,7 @@ button { font-family: inherit; cursor: pointer; }
1101
1178
  color: var(--text);
1102
1179
  }
1103
1180
  .mem-item .desc {
1104
- font-size: 11.5px; color: var(--text2);
1181
+ font-size: var(--fs-caption); color: var(--text2);
1105
1182
  margin-top: 2px;
1106
1183
  }
1107
1184
  .mem-item .meta {
@@ -1338,11 +1415,11 @@ button { font-family: inherit; cursor: pointer; }
1338
1415
  .cost-cell .v {
1339
1416
  font-family: var(--serif); font-weight: 500;
1340
1417
  font-variant-numeric: tabular-nums;
1341
- font-size: 26px; letter-spacing: -0.02em;
1418
+ font-size: var(--fs-num-s); letter-spacing: -0.02em;
1342
1419
  color: var(--text); line-height: 1.1;
1343
1420
  }
1344
1421
  .cost-cell .v small { font-size: 14px; color: var(--text2); margin-left: 2px; font-weight: 400; }
1345
- .cost-cell .sub { font-size: 11.5px; color: var(--text2); }
1422
+ .cost-cell .sub { font-size: var(--fs-caption); color: var(--text2); }
1346
1423
  .cost-cell.warn .v { color: var(--status-blocked); }
1347
1424
  .cost-cell.good .v { color: var(--status-review); }
1348
1425
 
@@ -1443,12 +1520,12 @@ button { font-family: inherit; cursor: pointer; }
1443
1520
  font-size: 12px;
1444
1521
  }
1445
1522
  .feat-name {
1446
- font-family: var(--mono); color: var(--text1);
1523
+ font-family: var(--mono); color: var(--text);
1447
1524
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1448
1525
  }
1449
- .feat-bar-wrap { background: var(--bg); border-radius: 3px; height: 6px; }
1526
+ .feat-bar-wrap { background: var(--bg-strong); border-radius: 3px; height: 6px; }
1450
1527
  .feat-bar { height: 6px; border-radius: 3px; background: var(--accent); transition: width .3s; }
1451
- .feat-cost { font-family: var(--mono); color: var(--text1); text-align: right; }
1528
+ .feat-cost { font-family: var(--mono); color: var(--text); text-align: right; }
1452
1529
  .feat-runs { font-size: 11px; color: var(--text3); text-align: right; }
1453
1530
 
1454
1531
  /* ── Inbox page ───────────────────────────────────────────────────────────── */
@@ -1476,8 +1553,10 @@ button { font-family: inherit; cursor: pointer; }
1476
1553
  display: flex; align-items: center; gap: 10px;
1477
1554
  }
1478
1555
  .inbox-summary .pill-stat .num {
1479
- font-family: var(--mono); font-weight: 600;
1480
- font-size: 24px; color: var(--text);
1556
+ font-family: var(--serif); font-weight: 500;
1557
+ font-variant-numeric: tabular-nums;
1558
+ font-size: var(--fs-num-s); letter-spacing: -0.02em;
1559
+ color: var(--text);
1481
1560
  }
1482
1561
  .inbox-summary .pill-stat .lbl {
1483
1562
  font-family: var(--mono); font-size: 10px;
@@ -1542,7 +1621,7 @@ button { font-family: inherit; cursor: pointer; }
1542
1621
  .resume-list .resume-item:hover { color: var(--text); }
1543
1622
  .resume-list .resume-item .ri-tag {
1544
1623
  flex-shrink: 0;
1545
- font-family: var(--mono); font-size: 9px;
1624
+ font-family: var(--mono); font-size: var(--fs-eyebrow);
1546
1625
  padding: 2px 5px; border-radius: 3px;
1547
1626
  letter-spacing: 0.04em;
1548
1627
  background: rgba(255,255,255,0.05);
@@ -1550,7 +1629,7 @@ button { font-family: inherit; cursor: pointer; }
1550
1629
  margin-top: 1px;
1551
1630
  }
1552
1631
  .resume-list .resume-item .ri-tag.ok { color: var(--accent); background: rgba(0,217,126,0.08); }
1553
- .resume-list .resume-item .ri-tag.fail { color: var(--p1); background: rgba(255,80,80,0.08); }
1632
+ .resume-list .resume-item .ri-tag.fail { color: var(--p1-fg); background: rgba(255,80,80,0.08); }
1554
1633
  .resume-list .resume-item .ri-text {
1555
1634
  flex: 1; min-width: 0;
1556
1635
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
@@ -1572,7 +1651,7 @@ button { font-family: inherit; cursor: pointer; }
1572
1651
  }
1573
1652
  .inbox-section-head h3 {
1574
1653
  font-family: var(--sans); font-weight: 600;
1575
- font-size: 14px; margin: 0; color: var(--text);
1654
+ font-size: var(--fs-title-s); margin: 0; color: var(--text);
1576
1655
  }
1577
1656
  .inbox-section-head .inbox-count {
1578
1657
  font-family: var(--mono); font-size: 11px;
@@ -1607,7 +1686,7 @@ button { font-family: inherit; cursor: pointer; }
1607
1686
  min-width: 0; overflow-wrap: anywhere; word-break: break-word;
1608
1687
  }
1609
1688
  .inbox-row .ttl {
1610
- font-size: 13.5px; color: var(--text);
1689
+ font-size: var(--fs-body); color: var(--text);
1611
1690
  font-weight: 500;
1612
1691
  display: flex;
1613
1692
  flex-direction: column;
@@ -1623,7 +1702,7 @@ button { font-family: inherit; cursor: pointer; }
1623
1702
  }
1624
1703
  .inbox-row .ttl .ttl-desc {
1625
1704
  font-weight: 400;
1626
- font-size: 12.5px;
1705
+ font-size: var(--fs-small);
1627
1706
  color: var(--text2);
1628
1707
  display: -webkit-box;
1629
1708
  -webkit-line-clamp: 2;
@@ -1649,7 +1728,7 @@ button { font-family: inherit; cursor: pointer; }
1649
1728
  .budgets-table td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
1650
1729
  /* A row has to answer three questions: what is this agent, what does it cost,
1651
1730
  over what period. The slug alone answered none of them. */
1652
- .budgets-table .bt-slug { font-family: var(--mono); font-size: 12.5px; }
1731
+ .budgets-table .bt-slug { font-family: var(--mono); font-size: var(--fs-small); }
1653
1732
  .budgets-table .bt-desc {
1654
1733
  font-size: 12px; color: var(--text2); margin-top: 2px;
1655
1734
  max-width: 52ch; text-wrap: pretty;
@@ -1753,7 +1832,7 @@ button { font-family: inherit; cursor: pointer; }
1753
1832
  so a figure that ticks over re-flows its own width and the number appears to
1754
1833
  breathe. Tabular figures cost nothing here and stop it. */
1755
1834
  font-variant-numeric: tabular-nums;
1756
- font-size: 52px; line-height: 1;
1835
+ font-size: var(--fs-num-xl); line-height: 1;
1757
1836
  letter-spacing: -0.02em;
1758
1837
  }
1759
1838
  /* An absence is rendered quietly and never silently — the glyph stays out of the
@@ -1790,7 +1869,7 @@ button { font-family: inherit; cursor: pointer; }
1790
1869
  gap: 12px; margin-bottom: 28px;
1791
1870
  }
1792
1871
  .mp-secondary .metric-card { padding: 18px; }
1793
- .mp-secondary .metric-num { font-size: 30px; }
1872
+ .mp-secondary .metric-num { font-size: var(--fs-num-m); }
1794
1873
 
1795
1874
  .mp-row {
1796
1875
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
@@ -1801,7 +1880,7 @@ button { font-family: inherit; cursor: pointer; }
1801
1880
  }
1802
1881
  .mp-card h3 {
1803
1882
  font-family: var(--sans); font-weight: 600;
1804
- font-size: 15px; margin: 0 0 16px;
1883
+ font-size: var(--fs-title-s); margin: 0 0 16px;
1805
1884
  }
1806
1885
  .mp-card-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 16px; }
1807
1886
  .mp-card-head h3 { margin: 0; }
@@ -1821,7 +1900,7 @@ button { font-family: inherit; cursor: pointer; }
1821
1900
  .bar-row {
1822
1901
  display: grid; grid-template-columns: 130px 1fr 50px;
1823
1902
  align-items: center; gap: 12px;
1824
- margin-bottom: 10px; font-size: 12.5px;
1903
+ margin-bottom: 10px; font-size: var(--fs-small);
1825
1904
  }
1826
1905
  .bar-row .name { font-family: var(--mono); color: var(--text); }
1827
1906
  .bar-row .rail {
@@ -1863,7 +1942,7 @@ button { font-family: inherit; cursor: pointer; }
1863
1942
  /* ── Logs tab ───────────────────────────────────────────── */
1864
1943
  .logs-page { display: flex; flex-direction: column; height: 100%; }
1865
1944
  .logs-header { padding: 20px 24px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: baseline; gap: 16px; }
1866
- .logs-header h2 { margin: 0; font-size: 18px; }
1945
+ .logs-header h2 { margin: 0; font-size: var(--fs-title-l); letter-spacing: -0.02em; }
1867
1946
  .logs-hint { font-size: 12px; color: var(--text3); }
1868
1947
  .logs-hint code { font-family: var(--mono); background: var(--bg-strong); padding: 1px 5px; border-radius: 3px; }
1869
1948
  .logs-layout { display: flex; flex: 1; overflow: hidden; }
@@ -1890,7 +1969,7 @@ button { font-family: inherit; cursor: pointer; }
1890
1969
  .log-item .log-title { font-size: 13px; font-weight: 500; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1891
1970
  .log-item .log-meta { font-size: 11px; color: var(--text3); margin-top: 2px; }
1892
1971
  .log-detail-head { margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
1893
- .log-detail-head h3 { margin: 0 0 4px; font-size: 17px; }
1972
+ .log-detail-head h3 { margin: 0 0 4px; font-size: var(--fs-title-m); letter-spacing: -0.01em; }
1894
1973
  .log-detail-head .log-detail-meta { font-size: 12px; color: var(--text3); }
1895
1974
  .log-section { margin-bottom: 18px; }
1896
1975
  .log-section-label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
@@ -2086,7 +2165,8 @@ button { font-family: inherit; cursor: pointer; }
2086
2165
  padding: 16px 20px; border-bottom: 1px solid var(--border);
2087
2166
  }
2088
2167
  .agent-drawer-header h3 {
2089
- font-family: var(--mono); font-size: 14px;
2168
+ font-family: var(--sans); font-weight: 600;
2169
+ font-size: var(--fs-title-s);
2090
2170
  color: var(--text); flex: 1; margin: 0;
2091
2171
  }
2092
2172
  .agent-drawer-close {
@@ -2169,7 +2249,7 @@ button { font-family: inherit; cursor: pointer; }
2169
2249
  border-radius: 16px;
2170
2250
  padding: 32px;
2171
2251
  }
2172
- .share-card h2 {
2252
+ .share-card h2:not(.section-title) {
2173
2253
  font-family: var(--serif); font-weight: 500;
2174
2254
  font-size: 28px; letter-spacing: -0.01em;
2175
2255
  margin: 0 0 6px;
@@ -2187,10 +2267,10 @@ button { font-family: inherit; cursor: pointer; }
2187
2267
  .share-toggle-row .label-grp .l { font-weight: 600; font-size: 14px; }
2188
2268
  .share-toggle-row .label-grp .h { color: var(--text2); font-size: 12px; }
2189
2269
 
2190
- .notif-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; transition: border-color 0.15s; }
2270
+ .notif-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--bg-muted); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; transition: border-color 0.15s; }
2191
2271
  .notif-row:hover { border-color: var(--accent); }
2192
2272
  .notif-row .emoji { font-size: 16px; flex-shrink: 0; }
2193
- .notif-row strong { color: var(--text1); margin-right: 4px; }
2273
+ .notif-row strong { color: var(--text); margin-right: 4px; }
2194
2274
  .notif-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
2195
2275
 
2196
2276
  .toggle {
@@ -2224,7 +2304,7 @@ button { font-family: inherit; cursor: pointer; }
2224
2304
  }
2225
2305
  .url-block .url-text {
2226
2306
  flex: 1; font-family: var(--mono);
2227
- font-size: 12.5px; color: var(--text);
2307
+ font-size: var(--fs-small); color: var(--text);
2228
2308
  white-space: nowrap; overflow: hidden;
2229
2309
  text-overflow: ellipsis;
2230
2310
  }
@@ -2357,7 +2437,7 @@ button { font-family: inherit; cursor: pointer; }
2357
2437
  display: grid;
2358
2438
  grid-template-columns: 130px 1fr 70px;
2359
2439
  align-items: center; gap: 12px;
2360
- margin-bottom: 10px; font-size: 12.5px;
2440
+ margin-bottom: 10px; font-size: var(--fs-small);
2361
2441
  }
2362
2442
  .agent-cost-row .name { font-family: var(--mono); color: var(--text); }
2363
2443
  .agent-cost-row .rail { display: block; height: 6px; background: var(--bg-strong); border-radius: 999px; overflow: hidden; }
@@ -2390,7 +2470,11 @@ button { font-family: inherit; cursor: pointer; }
2390
2470
  .side-section-head { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text3); margin-bottom: 8px; margin-top: 16px; }
2391
2471
 
2392
2472
  /* ── Security tab: hero KPI tile + activity feed ──────────────────────────── */
2393
- .kpi-hero .kpi-value-hero { font-size: 36px; line-height: 1.1; font-weight: 500; }
2473
+ .kpi-hero .kpi-value-hero {
2474
+ font-family: var(--serif); font-weight: 500;
2475
+ font-variant-numeric: tabular-nums;
2476
+ font-size: var(--fs-num-l); line-height: 1.1; letter-spacing: -0.02em;
2477
+ }
2394
2478
  .kpi-hero .kpi-label { font-size: 13px; font-weight: 500; }
2395
2479
  .kpi-empty .kpi-value { color: var(--accent); font-size: 22px; }
2396
2480
  .kpi-empty .kpi-value::after {
@@ -2745,6 +2829,7 @@ button { font-family: inherit; cursor: pointer; }
2745
2829
 
2746
2830
  <!-- Kanban -->
2747
2831
  <div class="panel" id="panel-kanban">
2832
+ <h1 class="page-title">Tasks</h1>
2748
2833
  <div id="filter-bar" class="filter-bar"></div>
2749
2834
  <div class="kanban" id="kanban-board"></div>
2750
2835
  </div>
@@ -2753,6 +2838,7 @@ button { font-family: inherit; cursor: pointer; }
2753
2838
  <!-- Metrics -->
2754
2839
  <div class="panel" id="panel-dashboard">
2755
2840
  <div class="metrics-page">
2841
+ <h1 class="page-title">Metrics</h1>
2756
2842
  <div class="period-toolbar">
2757
2843
  <span class="period-label">PERIOD</span>
2758
2844
  <div class="period-chips" id="period-chips">
@@ -2898,7 +2984,7 @@ button { font-family: inherit; cursor: pointer; }
2898
2984
 
2899
2985
  <div class="panel" id="panel-budgets" role="tabpanel">
2900
2986
  <div class="metrics-page">
2901
- <h1>Agent budgets</h1>
2987
+ <h1 class="page-title">Agent budgets</h1>
2902
2988
  <p class="muted" style="max-width:70ch;margin:6px 0 20px">
2903
2989
  A cap on what one agent may spend. When measured spend passes it, the pipeline
2904
2990
  refuses to dispatch that agent and says so — it does not fail quietly.
@@ -2910,7 +2996,7 @@ button { font-family: inherit; cursor: pointer; }
2910
2996
  </div>
2911
2997
 
2912
2998
  <div class="panel" id="panel-docs">
2913
- <h1>Docs</h1>
2999
+ <h1 class="page-title">Docs</h1>
2914
3000
  <div class="docs-search">
2915
3001
  <input type="search" id="docs-q" placeholder="Search inside documents…"
2916
3002
  aria-label="Search inside documents"
@@ -2979,8 +3065,9 @@ button { font-family: inherit; cursor: pointer; }
2979
3065
  <!-- Notifications -->
2980
3066
  <div class="panel" id="panel-notifications">
2981
3067
  <div class="share-page">
3068
+ <h1 class="page-title">Notifications</h1>
2982
3069
  <div class="share-card">
2983
- <h2>Email alerts</h2>
3070
+ <h2 class="section-title">Email alerts</h2>
2984
3071
  <p class="sub">Get notified when something needs your attention — even when you're away from the board. Verify your email once, pick what to receive, done. No API keys, no extra signup.</p>
2985
3072
 
2986
3073
  <div class="share-toggle-row">
@@ -2997,7 +3084,7 @@ button { font-family: inherit; cursor: pointer; }
2997
3084
  <div id="notif-step-email">
2998
3085
  <div style="font-size:11px;color:var(--text3);margin-bottom:4px;font-family:var(--mono);letter-spacing:.05em">RECIPIENT EMAIL</div>
2999
3086
  <div style="display:flex;gap:8px;flex-wrap:wrap">
3000
- <input id="notif-to" type="email" placeholder="you@example.com" style="flex:1 1 280px;min-width:240px;padding:8px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;color:var(--text1);font-size:14px" />
3087
+ <input id="notif-to" type="email" placeholder="you@example.com" style="flex:1 1 280px;min-width:240px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px" />
3001
3088
  <button class="btn-black" onclick="sendVerifyCode()">Send code</button>
3002
3089
  </div>
3003
3090
  </div>
@@ -3006,23 +3093,23 @@ button { font-family: inherit; cursor: pointer; }
3006
3093
  <div id="notif-step-code" style="display:none;margin-top:14px">
3007
3094
  <div style="font-size:11px;color:var(--text3);margin-bottom:4px;font-family:var(--mono);letter-spacing:.05em">VERIFICATION CODE (CHECK <span id="notif-code-target">YOUR INBOX</span>)</div>
3008
3095
  <div style="display:flex;gap:8px;flex-wrap:wrap">
3009
- <input id="notif-code" type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" placeholder="123456" style="flex:1 1 200px;min-width:140px;padding:8px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;color:var(--text1);font-size:18px;font-family:var(--mono);letter-spacing:.2em;text-align:center" />
3096
+ <input id="notif-code" type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" placeholder="123456" style="flex:1 1 200px;min-width:140px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:18px;font-family:var(--mono);letter-spacing:.2em;text-align:center" />
3010
3097
  <button class="btn-black" onclick="confirmVerifyCode()">Verify</button>
3011
3098
  <button class="btn-ghost-sm" onclick="sendVerifyCode()">Resend code</button>
3012
3099
  </div>
3013
3100
  </div>
3014
3101
 
3015
3102
  <!-- Step 3: verified -->
3016
- <div id="notif-step-verified" style="display:none;margin-top:14px;padding:10px 14px;background:rgba(0,217,126,0.08);border:1px solid rgba(0,217,126,0.30);border-radius:8px;display:none;align-items:center;gap:10px;font-size:13.5px">
3103
+ <div id="notif-step-verified" style="display:none;margin-top:14px;padding:10px 14px;background:rgba(0,217,126,0.08);border:1px solid rgba(0,217,126,0.30);border-radius:8px;display:none;align-items:center;gap:10px;font-size: var(--fs-body)">
3017
3104
  <span style="color:var(--accent);font-size:18px">✓</span>
3018
3105
  <span><strong id="notif-verified-email">—</strong> verified · alerts delivered through <code>greatcto.systems/notify</code></span>
3019
3106
  <button class="btn-ghost-sm" style="margin-left:auto" onclick="changeEmail()">Change</button>
3020
3107
  </div>
3021
3108
 
3022
3109
  <div class="share-note" style="margin-top:24px">
3023
- <strong style="color:var(--text1)">Which alerts to send</strong>
3110
+ <strong style="color:var(--text)">Which alerts to send</strong>
3024
3111
  </div>
3025
- <div id="notif-triggers" style="display:flex;flex-direction:column;gap:10px;margin-top:10px;font-size:13.5px">
3112
+ <div id="notif-triggers" style="display:flex;flex-direction:column;gap:10px;margin-top:10px;font-size: var(--fs-body)">
3026
3113
  <label class="notif-row"><input type="checkbox" data-trigger="incident.p0"> <span class="emoji">🚨</span><strong>P0 incident</strong> — production failure, ack-grade</label>
3027
3114
  <label class="notif-row"><input type="checkbox" data-trigger="gate.stale"> <span class="emoji">⏸️</span><strong>Gate stale &gt; 2h</strong> — ship gate waiting for your approval</label>
3028
3115
  <label class="notif-row"><input type="checkbox" data-trigger="gate.blocked"> <span class="emoji">🛡️</span><strong>Security BLOCKED</strong> — security-officer rejected a merge</label>
@@ -3044,7 +3131,7 @@ button { font-family: inherit; cursor: pointer; }
3044
3131
 
3045
3132
  <!-- ── Browser push card ───────────────────────────────────────── -->
3046
3133
  <div class="share-card" style="margin-top:16px" id="push-card">
3047
- <h2>Browser push</h2>
3134
+ <h2 class="section-title">Browser push</h2>
3048
3135
  <p class="sub">Native desktop notifications — works even when the board tab is in the background. No email required. Uses Web Push (VAPID) — keys stay local.</p>
3049
3136
  <div class="share-toggle-row">
3050
3137
  <div class="label-grp">
@@ -3055,7 +3142,7 @@ button { font-family: inherit; cursor: pointer; }
3055
3142
  <input type="checkbox" id="push-toggle" onclick="event.stopPropagation()">
3056
3143
  </div>
3057
3144
  </div>
3058
- <div id="push-status" style="margin-top:10px;font-size:12.5px;font-family:var(--mono);color:var(--text3)"></div>
3145
+ <div id="push-status" style="margin-top:10px;font-size: var(--fs-small);font-family:var(--mono);color:var(--text3)"></div>
3059
3146
  <div class="share-note" style="margin-top:16px">
3060
3147
  Push subscriptions are stored locally in <code>~/.great_cto/push-subscriptions.json</code>. Notifications fire for the same 5 triggers as email alerts.
3061
3148
  </div>
@@ -3063,7 +3150,7 @@ button { font-family: inherit; cursor: pointer; }
3063
3150
 
3064
3151
  <!-- ── In-app notification history ─────────────────────────────── -->
3065
3152
  <div class="share-card" style="margin-top:16px">
3066
- <h2>Recent notifications</h2>
3153
+ <h2 class="section-title">Recent notifications</h2>
3067
3154
  <p class="sub">Last 100 alerts fired this session. Also accessible via the notification drawer (bell icon).</p>
3068
3155
  <div id="notif-history-inline" style="display:flex;flex-direction:column;gap:0;margin-top:12px;
3069
3156
  border:1px solid var(--border);border-radius:8px;overflow:hidden;font-size:13px">
@@ -6197,15 +6284,22 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSide();
6197
6284
  */
6198
6285
  async function checkBoardFreshness() {
6199
6286
  const v = await api('/api/version');
6200
- if (!v || v.stale !== 'yes') return;
6287
+ // `behind` and `ahead` are different problems with different fixes; `no` and
6288
+ // `unknown` are both silent, because a comparison we could not make must not
6289
+ // render as a mismatch.
6290
+ if (!v || (v.stale !== 'behind' && v.stale !== 'ahead')) return;
6201
6291
  const host = document.querySelector('.workspace');
6202
6292
  if (!host || document.getElementById('stale-board')) return;
6203
6293
  const el = document.createElement('div');
6204
6294
  el.id = 'stale-board';
6205
6295
  el.className = 'stale-board';
6206
6296
  el.setAttribute('role', 'status');
6207
- el.innerHTML = `<span>This board is running <code>v${esc(v.version)}</code> `
6208
- + `<code>v${esc(v.installed)}</code> is installed. Restart it to pick up the new version.</span>`;
6297
+ el.innerHTML = v.stale === 'behind'
6298
+ ? `<span>This board is running <code>v${esc(v.version)}</code> `
6299
+ + `<code>v${esc(v.installed)}</code> is installed. Restart it to pick up the new version.</span>`
6300
+ : `<span>This board is running <code>v${esc(v.version)}</code>, which is newer than the `
6301
+ + `installed <code>v${esc(v.installed)}</code>. Restarting would go back to the older build — `
6302
+ + `install this version first.</span>`;
6209
6303
  host.insertBefore(el, host.firstChild);
6210
6304
  }
6211
6305
 
@@ -7236,7 +7330,7 @@ function renderNotifDrawer() {
7236
7330
  <div style="display:flex;align-items:flex-start;gap:10px">
7237
7331
  <span style="font-size:15px;flex-shrink:0;margin-top:1px">${_notifLevelIcon(n.level)}</span>
7238
7332
  <div style="flex:1;min-width:0">
7239
- <div style="font-size:13px;font-weight:${n.read ? '400' : '600'};color:var(--text1);
7333
+ <div style="font-size:13px;font-weight:${n.read ? '400' : '600'};color:var(--text);
7240
7334
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(n.title)}</div>
7241
7335
  <div style="font-size:12px;color:var(--text3);margin-top:2px;line-height:1.4">${esc(n.body)}</div>
7242
7336
  <div style="font-size:11px;color:var(--text3);margin-top:4px;font-family:var(--mono)">
@@ -7416,7 +7510,7 @@ if (_costLbl) _costLbl.textContent = `Cost · last ${periodLabel(currentPeriod)}
7416
7510
 
7417
7511
  <!-- ── Notification drawer ──────────────────────────────────────────────── -->
7418
7512
  <div id="notif-drawer" style="position:fixed;top:0;right:-400px;width:380px;height:100vh;
7419
- background:var(--bg1);border-left:1px solid var(--border);z-index:9999;
7513
+ background:var(--bg-elevated);border-left:1px solid var(--border);z-index:9999;
7420
7514
  transition:right .25s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;
7421
7515
  box-shadow:-4px 0 32px rgba(0,0,0,.5)">
7422
7516
  <div style="padding:14px 16px;border-bottom:1px solid var(--border);
@@ -7425,7 +7519,7 @@ if (_costLbl) _costLbl.textContent = `Cost · last ${periodLabel(currentPeriod)}
7425
7519
  style="width:16px;height:16px;color:var(--text2)">
7426
7520
  <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9 M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
7427
7521
  </svg>
7428
- <span style="font-weight:600;font-size:14px;flex:1;color:var(--text1)">Notifications</span>
7522
+ <span style="font-weight:600;font-size:14px;flex:1;color:var(--text)">Notifications</span>
7429
7523
  <button class="btn-ghost-sm" onclick="markAllNotifsRead()"
7430
7524
  style="font-size:12px">Mark all read</button>
7431
7525
  <button onclick="closeNotifDrawer()" style="background:none;border:none;cursor:pointer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",