great-cto 3.1.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
|
-
-
|
|
86
|
-
-
|
|
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.
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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:
|
|
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(--
|
|
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(--
|
|
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
|
|
637
|
-
#agent-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--
|
|
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(--
|
|
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(--
|
|
737
|
-
font-size:
|
|
738
|
-
letter-spacing: -0.
|
|
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:
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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:
|
|
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(--
|
|
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:
|
|
956
|
-
text-transform: uppercase; letter-spacing: 0.
|
|
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(--
|
|
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(--
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(--
|
|
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(--
|
|
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(--
|
|
1480
|
-
font-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1705
|
+
font-size: var(--fs-small);
|
|
1627
1706
|
color: var(--text2);
|
|
1628
1707
|
display: -webkit-box;
|
|
1629
1708
|
-webkit-line-clamp: 2;
|
|
@@ -1647,8 +1726,20 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1647
1726
|
padding: 0 12px 8px 0; border-bottom: 1px solid var(--border);
|
|
1648
1727
|
}
|
|
1649
1728
|
.budgets-table td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
1650
|
-
|
|
1651
|
-
|
|
1729
|
+
/* A row has to answer three questions: what is this agent, what does it cost,
|
|
1730
|
+
over what period. The slug alone answered none of them. */
|
|
1731
|
+
.budgets-table .bt-slug { font-family: var(--mono); font-size: var(--fs-small); }
|
|
1732
|
+
.budgets-table .bt-desc {
|
|
1733
|
+
font-size: 12px; color: var(--text2); margin-top: 2px;
|
|
1734
|
+
max-width: 52ch; text-wrap: pretty;
|
|
1735
|
+
}
|
|
1736
|
+
.budgets-table .bt-runs,
|
|
1737
|
+
.budgets-table .bt-cap { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
|
|
1738
|
+
.budgets-table .bt-spend { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
|
|
1739
|
+
.budgets-table .bt-usd { font-variant-numeric: tabular-nums; color: var(--text); }
|
|
1740
|
+
.budgets-table th[title] { cursor: help; }
|
|
1741
|
+
.budgets-idle summary { cursor: pointer; font-size: 13px; color: var(--text2); }
|
|
1742
|
+
.budgets-idle summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
|
|
1652
1743
|
.budgets-table .bt-actions { text-align: right; white-space: nowrap; }
|
|
1653
1744
|
.budgets-table .bt-actions .ab-btn + .ab-btn { margin-left: 6px; }
|
|
1654
1745
|
.budgets-empty {
|
|
@@ -1741,7 +1832,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1741
1832
|
so a figure that ticks over re-flows its own width and the number appears to
|
|
1742
1833
|
breathe. Tabular figures cost nothing here and stop it. */
|
|
1743
1834
|
font-variant-numeric: tabular-nums;
|
|
1744
|
-
font-size:
|
|
1835
|
+
font-size: var(--fs-num-xl); line-height: 1;
|
|
1745
1836
|
letter-spacing: -0.02em;
|
|
1746
1837
|
}
|
|
1747
1838
|
/* An absence is rendered quietly and never silently — the glyph stays out of the
|
|
@@ -1778,7 +1869,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1778
1869
|
gap: 12px; margin-bottom: 28px;
|
|
1779
1870
|
}
|
|
1780
1871
|
.mp-secondary .metric-card { padding: 18px; }
|
|
1781
|
-
.mp-secondary .metric-num { font-size:
|
|
1872
|
+
.mp-secondary .metric-num { font-size: var(--fs-num-m); }
|
|
1782
1873
|
|
|
1783
1874
|
.mp-row {
|
|
1784
1875
|
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
|
|
@@ -1789,7 +1880,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1789
1880
|
}
|
|
1790
1881
|
.mp-card h3 {
|
|
1791
1882
|
font-family: var(--sans); font-weight: 600;
|
|
1792
|
-
font-size:
|
|
1883
|
+
font-size: var(--fs-title-s); margin: 0 0 16px;
|
|
1793
1884
|
}
|
|
1794
1885
|
.mp-card-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 16px; }
|
|
1795
1886
|
.mp-card-head h3 { margin: 0; }
|
|
@@ -1809,7 +1900,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1809
1900
|
.bar-row {
|
|
1810
1901
|
display: grid; grid-template-columns: 130px 1fr 50px;
|
|
1811
1902
|
align-items: center; gap: 12px;
|
|
1812
|
-
margin-bottom: 10px; font-size:
|
|
1903
|
+
margin-bottom: 10px; font-size: var(--fs-small);
|
|
1813
1904
|
}
|
|
1814
1905
|
.bar-row .name { font-family: var(--mono); color: var(--text); }
|
|
1815
1906
|
.bar-row .rail {
|
|
@@ -1851,7 +1942,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1851
1942
|
/* ── Logs tab ───────────────────────────────────────────── */
|
|
1852
1943
|
.logs-page { display: flex; flex-direction: column; height: 100%; }
|
|
1853
1944
|
.logs-header { padding: 20px 24px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: baseline; gap: 16px; }
|
|
1854
|
-
.logs-header h2 { margin: 0; font-size:
|
|
1945
|
+
.logs-header h2 { margin: 0; font-size: var(--fs-title-l); letter-spacing: -0.02em; }
|
|
1855
1946
|
.logs-hint { font-size: 12px; color: var(--text3); }
|
|
1856
1947
|
.logs-hint code { font-family: var(--mono); background: var(--bg-strong); padding: 1px 5px; border-radius: 3px; }
|
|
1857
1948
|
.logs-layout { display: flex; flex: 1; overflow: hidden; }
|
|
@@ -1878,7 +1969,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
1878
1969
|
.log-item .log-title { font-size: 13px; font-weight: 500; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1879
1970
|
.log-item .log-meta { font-size: 11px; color: var(--text3); margin-top: 2px; }
|
|
1880
1971
|
.log-detail-head { margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
|
|
1881
|
-
.log-detail-head h3 { margin: 0 0 4px; font-size:
|
|
1972
|
+
.log-detail-head h3 { margin: 0 0 4px; font-size: var(--fs-title-m); letter-spacing: -0.01em; }
|
|
1882
1973
|
.log-detail-head .log-detail-meta { font-size: 12px; color: var(--text3); }
|
|
1883
1974
|
.log-section { margin-bottom: 18px; }
|
|
1884
1975
|
.log-section-label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
|
|
@@ -2074,7 +2165,8 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2074
2165
|
padding: 16px 20px; border-bottom: 1px solid var(--border);
|
|
2075
2166
|
}
|
|
2076
2167
|
.agent-drawer-header h3 {
|
|
2077
|
-
font-family: var(--
|
|
2168
|
+
font-family: var(--sans); font-weight: 600;
|
|
2169
|
+
font-size: var(--fs-title-s);
|
|
2078
2170
|
color: var(--text); flex: 1; margin: 0;
|
|
2079
2171
|
}
|
|
2080
2172
|
.agent-drawer-close {
|
|
@@ -2157,7 +2249,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2157
2249
|
border-radius: 16px;
|
|
2158
2250
|
padding: 32px;
|
|
2159
2251
|
}
|
|
2160
|
-
.share-card h2 {
|
|
2252
|
+
.share-card h2:not(.section-title) {
|
|
2161
2253
|
font-family: var(--serif); font-weight: 500;
|
|
2162
2254
|
font-size: 28px; letter-spacing: -0.01em;
|
|
2163
2255
|
margin: 0 0 6px;
|
|
@@ -2175,10 +2267,10 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2175
2267
|
.share-toggle-row .label-grp .l { font-weight: 600; font-size: 14px; }
|
|
2176
2268
|
.share-toggle-row .label-grp .h { color: var(--text2); font-size: 12px; }
|
|
2177
2269
|
|
|
2178
|
-
.notif-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--
|
|
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; }
|
|
2179
2271
|
.notif-row:hover { border-color: var(--accent); }
|
|
2180
2272
|
.notif-row .emoji { font-size: 16px; flex-shrink: 0; }
|
|
2181
|
-
.notif-row strong { color: var(--
|
|
2273
|
+
.notif-row strong { color: var(--text); margin-right: 4px; }
|
|
2182
2274
|
.notif-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
|
|
2183
2275
|
|
|
2184
2276
|
.toggle {
|
|
@@ -2212,7 +2304,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2212
2304
|
}
|
|
2213
2305
|
.url-block .url-text {
|
|
2214
2306
|
flex: 1; font-family: var(--mono);
|
|
2215
|
-
font-size:
|
|
2307
|
+
font-size: var(--fs-small); color: var(--text);
|
|
2216
2308
|
white-space: nowrap; overflow: hidden;
|
|
2217
2309
|
text-overflow: ellipsis;
|
|
2218
2310
|
}
|
|
@@ -2345,7 +2437,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2345
2437
|
display: grid;
|
|
2346
2438
|
grid-template-columns: 130px 1fr 70px;
|
|
2347
2439
|
align-items: center; gap: 12px;
|
|
2348
|
-
margin-bottom: 10px; font-size:
|
|
2440
|
+
margin-bottom: 10px; font-size: var(--fs-small);
|
|
2349
2441
|
}
|
|
2350
2442
|
.agent-cost-row .name { font-family: var(--mono); color: var(--text); }
|
|
2351
2443
|
.agent-cost-row .rail { display: block; height: 6px; background: var(--bg-strong); border-radius: 999px; overflow: hidden; }
|
|
@@ -2378,7 +2470,11 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2378
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; }
|
|
2379
2471
|
|
|
2380
2472
|
/* ── Security tab: hero KPI tile + activity feed ──────────────────────────── */
|
|
2381
|
-
.kpi-hero .kpi-value-hero {
|
|
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
|
+
}
|
|
2382
2478
|
.kpi-hero .kpi-label { font-size: 13px; font-weight: 500; }
|
|
2383
2479
|
.kpi-empty .kpi-value { color: var(--accent); font-size: 22px; }
|
|
2384
2480
|
.kpi-empty .kpi-value::after {
|
|
@@ -2733,6 +2829,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2733
2829
|
|
|
2734
2830
|
<!-- Kanban -->
|
|
2735
2831
|
<div class="panel" id="panel-kanban">
|
|
2832
|
+
<h1 class="page-title">Tasks</h1>
|
|
2736
2833
|
<div id="filter-bar" class="filter-bar"></div>
|
|
2737
2834
|
<div class="kanban" id="kanban-board"></div>
|
|
2738
2835
|
</div>
|
|
@@ -2741,6 +2838,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2741
2838
|
<!-- Metrics -->
|
|
2742
2839
|
<div class="panel" id="panel-dashboard">
|
|
2743
2840
|
<div class="metrics-page">
|
|
2841
|
+
<h1 class="page-title">Metrics</h1>
|
|
2744
2842
|
<div class="period-toolbar">
|
|
2745
2843
|
<span class="period-label">PERIOD</span>
|
|
2746
2844
|
<div class="period-chips" id="period-chips">
|
|
@@ -2886,7 +2984,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2886
2984
|
|
|
2887
2985
|
<div class="panel" id="panel-budgets" role="tabpanel">
|
|
2888
2986
|
<div class="metrics-page">
|
|
2889
|
-
<h1>Agent budgets</h1>
|
|
2987
|
+
<h1 class="page-title">Agent budgets</h1>
|
|
2890
2988
|
<p class="muted" style="max-width:70ch;margin:6px 0 20px">
|
|
2891
2989
|
A cap on what one agent may spend. When measured spend passes it, the pipeline
|
|
2892
2990
|
refuses to dispatch that agent and says so — it does not fail quietly.
|
|
@@ -2898,7 +2996,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2898
2996
|
</div>
|
|
2899
2997
|
|
|
2900
2998
|
<div class="panel" id="panel-docs">
|
|
2901
|
-
<h1>Docs</h1>
|
|
2999
|
+
<h1 class="page-title">Docs</h1>
|
|
2902
3000
|
<div class="docs-search">
|
|
2903
3001
|
<input type="search" id="docs-q" placeholder="Search inside documents…"
|
|
2904
3002
|
aria-label="Search inside documents"
|
|
@@ -2967,8 +3065,9 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2967
3065
|
<!-- Notifications -->
|
|
2968
3066
|
<div class="panel" id="panel-notifications">
|
|
2969
3067
|
<div class="share-page">
|
|
3068
|
+
<h1 class="page-title">Notifications</h1>
|
|
2970
3069
|
<div class="share-card">
|
|
2971
|
-
<h2>Email alerts</h2>
|
|
3070
|
+
<h2 class="section-title">Email alerts</h2>
|
|
2972
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>
|
|
2973
3072
|
|
|
2974
3073
|
<div class="share-toggle-row">
|
|
@@ -2985,7 +3084,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2985
3084
|
<div id="notif-step-email">
|
|
2986
3085
|
<div style="font-size:11px;color:var(--text3);margin-bottom:4px;font-family:var(--mono);letter-spacing:.05em">RECIPIENT EMAIL</div>
|
|
2987
3086
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
2988
|
-
<input id="notif-to" type="email" placeholder="you@example.com" style="flex:1 1 280px;min-width:240px;padding:8px 10px;background:var(--
|
|
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" />
|
|
2989
3088
|
<button class="btn-black" onclick="sendVerifyCode()">Send code</button>
|
|
2990
3089
|
</div>
|
|
2991
3090
|
</div>
|
|
@@ -2994,23 +3093,23 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2994
3093
|
<div id="notif-step-code" style="display:none;margin-top:14px">
|
|
2995
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>
|
|
2996
3095
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
2997
|
-
<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(--
|
|
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" />
|
|
2998
3097
|
<button class="btn-black" onclick="confirmVerifyCode()">Verify</button>
|
|
2999
3098
|
<button class="btn-ghost-sm" onclick="sendVerifyCode()">Resend code</button>
|
|
3000
3099
|
</div>
|
|
3001
3100
|
</div>
|
|
3002
3101
|
|
|
3003
3102
|
<!-- Step 3: verified -->
|
|
3004
|
-
<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:
|
|
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)">
|
|
3005
3104
|
<span style="color:var(--accent);font-size:18px">✓</span>
|
|
3006
3105
|
<span><strong id="notif-verified-email">—</strong> verified · alerts delivered through <code>greatcto.systems/notify</code></span>
|
|
3007
3106
|
<button class="btn-ghost-sm" style="margin-left:auto" onclick="changeEmail()">Change</button>
|
|
3008
3107
|
</div>
|
|
3009
3108
|
|
|
3010
3109
|
<div class="share-note" style="margin-top:24px">
|
|
3011
|
-
<strong style="color:var(--
|
|
3110
|
+
<strong style="color:var(--text)">Which alerts to send</strong>
|
|
3012
3111
|
</div>
|
|
3013
|
-
<div id="notif-triggers" style="display:flex;flex-direction:column;gap:10px;margin-top:10px;font-size:
|
|
3112
|
+
<div id="notif-triggers" style="display:flex;flex-direction:column;gap:10px;margin-top:10px;font-size: var(--fs-body)">
|
|
3014
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>
|
|
3015
3114
|
<label class="notif-row"><input type="checkbox" data-trigger="gate.stale"> <span class="emoji">⏸️</span><strong>Gate stale > 2h</strong> — ship gate waiting for your approval</label>
|
|
3016
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>
|
|
@@ -3032,7 +3131,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
3032
3131
|
|
|
3033
3132
|
<!-- ── Browser push card ───────────────────────────────────────── -->
|
|
3034
3133
|
<div class="share-card" style="margin-top:16px" id="push-card">
|
|
3035
|
-
<h2>Browser push</h2>
|
|
3134
|
+
<h2 class="section-title">Browser push</h2>
|
|
3036
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>
|
|
3037
3136
|
<div class="share-toggle-row">
|
|
3038
3137
|
<div class="label-grp">
|
|
@@ -3043,7 +3142,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
3043
3142
|
<input type="checkbox" id="push-toggle" onclick="event.stopPropagation()">
|
|
3044
3143
|
</div>
|
|
3045
3144
|
</div>
|
|
3046
|
-
<div id="push-status" style="margin-top:10px;font-size:
|
|
3145
|
+
<div id="push-status" style="margin-top:10px;font-size: var(--fs-small);font-family:var(--mono);color:var(--text3)"></div>
|
|
3047
3146
|
<div class="share-note" style="margin-top:16px">
|
|
3048
3147
|
Push subscriptions are stored locally in <code>~/.great_cto/push-subscriptions.json</code>. Notifications fire for the same 5 triggers as email alerts.
|
|
3049
3148
|
</div>
|
|
@@ -3051,7 +3150,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
3051
3150
|
|
|
3052
3151
|
<!-- ── In-app notification history ─────────────────────────────── -->
|
|
3053
3152
|
<div class="share-card" style="margin-top:16px">
|
|
3054
|
-
<h2>Recent notifications</h2>
|
|
3153
|
+
<h2 class="section-title">Recent notifications</h2>
|
|
3055
3154
|
<p class="sub">Last 100 alerts fired this session. Also accessible via the notification drawer (bell icon).</p>
|
|
3056
3155
|
<div id="notif-history-inline" style="display:flex;flex-direction:column;gap:0;margin-top:12px;
|
|
3057
3156
|
border:1px solid var(--border);border-radius:8px;overflow:hidden;font-size:13px">
|
|
@@ -3544,66 +3643,120 @@ async function loadBudgetsPage() {
|
|
|
3544
3643
|
|
|
3545
3644
|
const [h] = await Promise.all([
|
|
3546
3645
|
api(`/api/heartbeat${pqs()}`),
|
|
3547
|
-
(typeof
|
|
3646
|
+
(typeof refreshAgentsInstalled === 'function' && !(fleetState?.agents || []).length)
|
|
3647
|
+
? refreshAgentsInstalled() : Promise.resolve(),
|
|
3548
3648
|
]);
|
|
3549
3649
|
const caps = h?.budgets || {};
|
|
3550
|
-
const agents = fleetState?.agents || [];
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3650
|
+
const agents = (fleetState?.agents || []).filter((a) => !a.retired);
|
|
3651
|
+
|
|
3652
|
+
// 69 identical chips in one blob answered none of the three questions a person
|
|
3653
|
+
// arrives with: what does this agent do, what does it usually cost, and over
|
|
3654
|
+
// what period. Worse, 62 of the 69 had not run in thirty days — a cap on an
|
|
3655
|
+
// agent that never runs is a cap on nothing, and they were the bulk of the
|
|
3656
|
+
// wall.
|
|
3657
|
+
//
|
|
3658
|
+
// Ordered by what it costs to leave uncapped: agents that ran, most spend
|
|
3659
|
+
// first. The ones that never ran are still reachable, behind a count.
|
|
3660
|
+
const spendOf = (a) => (a.llm_usd_30d_real ?? a.llm_usd_30d_est ?? 0);
|
|
3661
|
+
const ran = agents.filter((a) => (a.runs_30d || 0) > 0)
|
|
3662
|
+
.sort((x, y) => spendOf(y) - spendOf(x) || (y.runs_30d || 0) - (x.runs_30d || 0));
|
|
3663
|
+
const idle = agents.filter((a) => !(a.runs_30d > 0));
|
|
3664
|
+
|
|
3665
|
+
const firstSentence = (t) => {
|
|
3666
|
+
const s1 = String(t || '').split(/(?<=[.!?])\s/)[0] || '';
|
|
3667
|
+
return s1.length > 150 ? s1.slice(0, 147) + '…' : s1;
|
|
3668
|
+
};
|
|
3669
|
+
|
|
3670
|
+
// Spend is stated with its window and its provenance. `est` is a time-based
|
|
3671
|
+
// guess and says so; a real figure only exists where a verdict carried a cost.
|
|
3672
|
+
const spendCell = (a) => {
|
|
3673
|
+
if (a.llm_usd_30d_real != null && a.llm_usd_30d_real > 0) {
|
|
3674
|
+
return `<span class="bt-usd">$${a.llm_usd_30d_real.toFixed(2)}</span> <span class="muted">measured</span>`;
|
|
3675
|
+
}
|
|
3676
|
+
if (a.llm_usd_30d_est != null && a.llm_usd_30d_est > 0) {
|
|
3677
|
+
return `<span class="bt-usd">$${a.llm_usd_30d_est.toFixed(2)}</span> <span class="muted">estimated</span>`;
|
|
3678
|
+
}
|
|
3679
|
+
return '<span class="muted">not measured</span>';
|
|
3680
|
+
};
|
|
3554
3681
|
|
|
3555
3682
|
const stateCell = (slug) => {
|
|
3556
|
-
const b =
|
|
3557
|
-
if (!b || b.state === 'no-limit') return '
|
|
3683
|
+
const b = (fleetState?.agents || []).find((a) => a.slug === slug)?.budget;
|
|
3684
|
+
if (!b || b.state === 'no-limit') return '';
|
|
3558
3685
|
const spent = (b.state === 'within' || b.state === 'exceeded')
|
|
3559
|
-
? `$${(b.measuredUsd ?? 0).toFixed(2)} measured`
|
|
3560
|
-
: (b.state === 'unmeasured' ? 'nothing measured yet' : '');
|
|
3686
|
+
? `$${(b.measuredUsd ?? 0).toFixed(2)} measured` : (b.state === 'unmeasured' ? 'nothing measured yet' : '');
|
|
3561
3687
|
return `${budgetChip(b)} <span class="ab-spent">${esc(spent)}</span>`;
|
|
3562
3688
|
};
|
|
3563
3689
|
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3690
|
+
const row = (a, cap) => `
|
|
3691
|
+
<tr>
|
|
3692
|
+
<td class="bt-agent">
|
|
3693
|
+
<div class="bt-slug">${esc(a.slug)}</div>
|
|
3694
|
+
<div class="bt-desc">${esc(firstSentence(a.description))}</div>
|
|
3695
|
+
</td>
|
|
3696
|
+
<td class="bt-runs">${a.runs_30d || 0}</td>
|
|
3697
|
+
<td class="bt-spend">${spendCell(a)}</td>
|
|
3698
|
+
<td class="bt-cap">${cap != null ? `$${esc(String(cap))}` : '<span class="muted">—</span>'}</td>
|
|
3699
|
+
<td class="bt-state">${cap != null ? stateCell(a.slug) : ''}</td>
|
|
3700
|
+
<td class="bt-actions">${cap != null
|
|
3701
|
+
? `<button type="button" class="ab-btn" onclick="editAgentBudget('${esc(a.slug)}', ${Number(cap)})">Change</button>`
|
|
3702
|
+
+ `<button type="button" class="ab-btn ab-rm" onclick="clearAgentBudget('${esc(a.slug)}', ${Number(cap)})">Remove</button>`
|
|
3703
|
+
: `<button type="button" class="ab-btn" onclick="addBudgetFor('${esc(a.slug)}')">Set a cap</button>`}</td>
|
|
3704
|
+
</tr>`;
|
|
3705
|
+
|
|
3706
|
+
const table = (rows) => `
|
|
3707
|
+
<table class="budgets-table">
|
|
3708
|
+
<thead><tr>
|
|
3709
|
+
<th scope="col">Agent</th>
|
|
3710
|
+
<th scope="col" title="Dispatches in the last 30 days">Runs 30d</th>
|
|
3711
|
+
<th scope="col" title="LLM spend over the same 30 days">Spend 30d</th>
|
|
3712
|
+
<th scope="col">Cap</th><th scope="col">State</th><th scope="col"></th>
|
|
3713
|
+
</tr></thead>
|
|
3714
|
+
<tbody>${rows}</tbody>
|
|
3715
|
+
</table>`;
|
|
3716
|
+
|
|
3717
|
+
// A cap declared for an agent the fleet does not list still has to appear —
|
|
3718
|
+
// otherwise a limit that is holding a stage would be invisible here.
|
|
3719
|
+
const known = new Set(agents.map((a) => a.slug));
|
|
3720
|
+
const orphanCaps = Object.entries(caps).filter(([slug]) => !known.has(slug));
|
|
3721
|
+
|
|
3722
|
+
const capped = ran.filter((a) => a.slug in caps);
|
|
3723
|
+
const uncappedRan = ran.filter((a) => !(a.slug in caps));
|
|
3724
|
+
|
|
3567
3725
|
const notes = [
|
|
3568
3726
|
h?.budgets_deprecated_key
|
|
3569
3727
|
? `<div class="ab-note">Read from the deprecated <code>${esc(h.budgets_deprecated_key)}:</code> key — rename it to <code>agent-budgets:</code>.</div>` : '',
|
|
3570
|
-
|
|
3571
|
-
? `<div class="ab-note ab-bad">${
|
|
3572
|
-
+
|
|
3728
|
+
(h?.budgets_malformed || []).length
|
|
3729
|
+
? `<div class="ab-note ab-bad">${h.budgets_malformed.length} line(s) in PROJECT.md were not read: `
|
|
3730
|
+
+ h.budgets_malformed.map((m) => `<code>${esc(m.line)}</code> — ${esc(m.why)}`).join('; ') + '</div>' : '',
|
|
3573
3731
|
].join('');
|
|
3574
3732
|
|
|
3575
|
-
const capsBlock = capped.length ? `
|
|
3576
|
-
<
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
<button type="button" class="ab-btn" onclick="addBudgetFor('${esc(a.slug)}')"
|
|
3597
|
-
title="Set a cap for ${esc(a.slug)}">${esc(a.slug)} +</button>`).join('')}
|
|
3598
|
-
${uncapped.length > 60 ? `<span class="muted">and ${uncapped.length - 60} more</span>` : ''}
|
|
3599
|
-
</div>` : '';
|
|
3733
|
+
const capsBlock = (capped.length || orphanCaps.length) ? `
|
|
3734
|
+
<div class="mp-section-label">Capped (${capped.length + orphanCaps.length})</div>
|
|
3735
|
+
${table(capped.map((a) => row(a, caps[a.slug])).join('')
|
|
3736
|
+
+ orphanCaps.map(([slug, cap]) => row({ slug, description: 'not among the installed agents', runs_30d: 0 }, cap)).join(''))}`
|
|
3737
|
+
: `<div class="budgets-empty">No agent has a spending cap in this project. Nothing is being held, and nothing will be.</div>`;
|
|
3738
|
+
|
|
3739
|
+
const activeBlock = uncappedRan.length ? `
|
|
3740
|
+
<div class="mp-section-label" style="margin-top:28px">Ran in the last 30 days, no cap (${uncappedRan.length})</div>
|
|
3741
|
+
<p class="muted" style="margin:4px 0 10px">These are the ones a cap would actually apply to.</p>
|
|
3742
|
+
${table(uncappedRan.map((a) => row(a, null)).join(''))}` : '';
|
|
3743
|
+
|
|
3744
|
+
// Capping an agent that has not run is capping nothing. Reachable, not shown.
|
|
3745
|
+
const idleBlock = idle.length ? `
|
|
3746
|
+
<details class="budgets-idle" style="margin-top:28px">
|
|
3747
|
+
<summary>${idle.length} installed agents have not run in 30 days</summary>
|
|
3748
|
+
<div class="budgets-uncapped">${idle.slice(0, 80).map((a) => `
|
|
3749
|
+
<button type="button" class="ab-btn" onclick="addBudgetFor('${esc(a.slug)}')"
|
|
3750
|
+
title="${esc(firstSentence(a.description))}">${esc(a.slug)} +</button>`).join('')}
|
|
3751
|
+
${idle.length > 80 ? `<span class="muted">and ${idle.length - 80} more</span>` : ''}
|
|
3752
|
+
</div>
|
|
3753
|
+
</details>` : '';
|
|
3600
3754
|
|
|
3601
|
-
body.innerHTML = `${notes}${capsBlock}
|
|
3602
|
-
<button type="button" class="ab-btn ab-add" onclick="addAgentBudget()">+ Add a budget</button
|
|
3603
|
-
${uncappedBlock}`;
|
|
3755
|
+
body.innerHTML = `${notes}${capsBlock}${activeBlock}${idleBlock}
|
|
3756
|
+
<button type="button" class="ab-btn ab-add" onclick="addAgentBudget()">+ Add a budget for any agent</button>`;
|
|
3604
3757
|
|
|
3605
3758
|
const badge = document.getElementById('nav-budget-count');
|
|
3606
|
-
if (badge) badge.textContent =
|
|
3759
|
+
if (badge) badge.textContent = Object.keys(caps).length ? String(Object.keys(caps).length) : '';
|
|
3607
3760
|
}
|
|
3608
3761
|
|
|
3609
3762
|
/** Set a cap for a named agent, skipping the "which agent?" question. */
|
|
@@ -6131,15 +6284,22 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSide();
|
|
|
6131
6284
|
*/
|
|
6132
6285
|
async function checkBoardFreshness() {
|
|
6133
6286
|
const v = await api('/api/version');
|
|
6134
|
-
|
|
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;
|
|
6135
6291
|
const host = document.querySelector('.workspace');
|
|
6136
6292
|
if (!host || document.getElementById('stale-board')) return;
|
|
6137
6293
|
const el = document.createElement('div');
|
|
6138
6294
|
el.id = 'stale-board';
|
|
6139
6295
|
el.className = 'stale-board';
|
|
6140
6296
|
el.setAttribute('role', 'status');
|
|
6141
|
-
el.innerHTML =
|
|
6142
|
-
|
|
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>`;
|
|
6143
6303
|
host.insertBefore(el, host.firstChild);
|
|
6144
6304
|
}
|
|
6145
6305
|
|
|
@@ -7170,7 +7330,7 @@ function renderNotifDrawer() {
|
|
|
7170
7330
|
<div style="display:flex;align-items:flex-start;gap:10px">
|
|
7171
7331
|
<span style="font-size:15px;flex-shrink:0;margin-top:1px">${_notifLevelIcon(n.level)}</span>
|
|
7172
7332
|
<div style="flex:1;min-width:0">
|
|
7173
|
-
<div style="font-size:13px;font-weight:${n.read ? '400' : '600'};color:var(--
|
|
7333
|
+
<div style="font-size:13px;font-weight:${n.read ? '400' : '600'};color:var(--text);
|
|
7174
7334
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(n.title)}</div>
|
|
7175
7335
|
<div style="font-size:12px;color:var(--text3);margin-top:2px;line-height:1.4">${esc(n.body)}</div>
|
|
7176
7336
|
<div style="font-size:11px;color:var(--text3);margin-top:4px;font-family:var(--mono)">
|
|
@@ -7350,7 +7510,7 @@ if (_costLbl) _costLbl.textContent = `Cost · last ${periodLabel(currentPeriod)}
|
|
|
7350
7510
|
|
|
7351
7511
|
<!-- ── Notification drawer ──────────────────────────────────────────────── -->
|
|
7352
7512
|
<div id="notif-drawer" style="position:fixed;top:0;right:-400px;width:380px;height:100vh;
|
|
7353
|
-
background:var(--
|
|
7513
|
+
background:var(--bg-elevated);border-left:1px solid var(--border);z-index:9999;
|
|
7354
7514
|
transition:right .25s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;
|
|
7355
7515
|
box-shadow:-4px 0 32px rgba(0,0,0,.5)">
|
|
7356
7516
|
<div style="padding:14px 16px;border-bottom:1px solid var(--border);
|
|
@@ -7359,7 +7519,7 @@ if (_costLbl) _costLbl.textContent = `Cost · last ${periodLabel(currentPeriod)}
|
|
|
7359
7519
|
style="width:16px;height:16px;color:var(--text2)">
|
|
7360
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"/>
|
|
7361
7521
|
</svg>
|
|
7362
|
-
<span style="font-weight:600;font-size:14px;flex:1;color:var(--
|
|
7522
|
+
<span style="font-weight:600;font-size:14px;flex:1;color:var(--text)">Notifications</span>
|
|
7363
7523
|
<button class="btn-ghost-sm" onclick="markAllNotifsRead()"
|
|
7364
7524
|
style="font-size:12px">Mark all read</button>
|
|
7365
7525
|
<button onclick="closeNotifDrawer()" style="background:none;border:none;cursor:pointer;
|
package/package.json
CHANGED