great-cto 2.98.0 → 2.99.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.
|
@@ -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": "2.
|
|
5
|
+
"version": "2.99.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Great CTO",
|
|
8
8
|
"url": "https://github.com/avelikiy/great_cto"
|
|
@@ -11,7 +11,18 @@ import { log } from './log.mjs';
|
|
|
11
11
|
// .beads/interactions.jsonl changes (the file watcher in watchBeads() calls
|
|
12
12
|
// bdCacheInvalidate(cwd) before broadcasting). This avoids spawning `bd list`
|
|
13
13
|
// on every API call when 5+ projects are open in tabs.
|
|
14
|
-
|
|
14
|
+
// Measured, not guessed: `bd list` on this repository takes 3.0–4.6 s and
|
|
15
|
+
// `bd --version` — pure process startup, no query — takes 1.13 s. At a 2 s TTL
|
|
16
|
+
// the entry expired before the call that filled it had returned, so a page load
|
|
17
|
+
// firing /api/inbox, /api/tasks and /api/metrics paid full price three times and
|
|
18
|
+
// every endpoint sat at ~5 s. A cache whose TTL is shorter than the operation it
|
|
19
|
+
// caches is not a cache; it is a counter of missed opportunities.
|
|
20
|
+
//
|
|
21
|
+
// 30 s is safe because staleness is bounded by invalidation, not by the clock:
|
|
22
|
+
// `bdCacheInvalidate` fires on every write through the board, and the file
|
|
23
|
+
// watchers fire when anything changes the store underneath us. The TTL is only
|
|
24
|
+
// the backstop for a change that arrived through neither.
|
|
25
|
+
const BD_CACHE_TTL_MS = Number(process.env.GREAT_CTO_BD_CACHE_TTL_MS || 30000);
|
|
15
26
|
|
|
16
27
|
function bdCacheInvalidate(cwd) { bdCache.delete(cwd); }
|
|
17
28
|
|
|
@@ -500,6 +511,7 @@ function detectAgent(task) {
|
|
|
500
511
|
|
|
501
512
|
export {
|
|
502
513
|
bdCacheInvalidate,
|
|
514
|
+
BD_CACHE_TTL_MS,
|
|
503
515
|
BD_BIN,
|
|
504
516
|
bdEnv,
|
|
505
517
|
bd,
|
|
@@ -1144,8 +1144,32 @@ async function dispatch(req, res, url, cwd) {
|
|
|
1144
1144
|
|
|
1145
1145
|
// Build version — so the board shows which great_cto version it's running.
|
|
1146
1146
|
if (pathname === '/api/version') {
|
|
1147
|
+
// A board is a process, and a process keeps the version it started with.
|
|
1148
|
+
// This one served v2.95.0 for nine days while three installs in a row
|
|
1149
|
+
// reported success: the installer updated files, the running server kept
|
|
1150
|
+
// answering, and nothing compared the two. The installer restarts the board
|
|
1151
|
+
// now, but a board started any other way still needs to be able to say that
|
|
1152
|
+
// it is behind — an install the operator cannot see is not an install.
|
|
1153
|
+
//
|
|
1154
|
+
// Three states, not two. `unknown` when the cache cannot be read: a version
|
|
1155
|
+
// we failed to look up must not render as agreement.
|
|
1156
|
+
let installed = null;
|
|
1157
|
+
try {
|
|
1158
|
+
const root = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'local', 'great_cto');
|
|
1159
|
+
const versions = fs.readdirSync(root).filter((d) => /^\d+\.\d+\.\d+$/.test(d));
|
|
1160
|
+
installed = versions.sort((a, b) => {
|
|
1161
|
+
const x = a.split('.').map(Number), y = b.split('.').map(Number);
|
|
1162
|
+
return (x[0] - y[0]) || (x[1] - y[1]) || (x[2] - y[2]);
|
|
1163
|
+
}).pop() || null;
|
|
1164
|
+
} catch { installed = null; }
|
|
1165
|
+
const stale = installed && BUILD_VERSION !== 'unknown'
|
|
1166
|
+
? (installed !== BUILD_VERSION ? 'yes' : 'no')
|
|
1167
|
+
: 'unknown';
|
|
1147
1168
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1148
|
-
res.end(JSON.stringify({
|
|
1169
|
+
res.end(JSON.stringify({
|
|
1170
|
+
version: BUILD_VERSION, installed, stale,
|
|
1171
|
+
surface: 'builder', node: process.version.replace(/^v/, ''),
|
|
1172
|
+
}));
|
|
1149
1173
|
return true;
|
|
1150
1174
|
}
|
|
1151
1175
|
|
|
@@ -153,6 +153,16 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
153
153
|
radial-gradient(ellipse 70% 50% at 100% 100%, rgba(0, 217, 126, 0.03), transparent 70%);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
.stale-board {
|
|
157
|
+
display: flex; align-items: center; gap: 10px;
|
|
158
|
+
margin: 0 0 12px; padding: 10px 14px;
|
|
159
|
+
background: rgba(180, 83, 9, 0.10);
|
|
160
|
+
border: 1px solid var(--status-progress);
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
font-size: 13px; color: var(--text);
|
|
163
|
+
}
|
|
164
|
+
.stale-board code { font-family: var(--mono); font-size: 12px; }
|
|
165
|
+
|
|
156
166
|
/* ── Sidebar ──────────────────────────────────────────────────────────────── */
|
|
157
167
|
.sidebar {
|
|
158
168
|
position: relative; z-index: 1;
|
|
@@ -564,11 +574,7 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
564
574
|
cannot be un-advanced. Scoped to `pointer: coarse` so the desktop density that
|
|
565
575
|
makes a long gate list scannable is left exactly as it is. 44px is the iOS
|
|
566
576
|
minimum; Android asks for 48dp, and the gap does the rest. */
|
|
567
|
-
|
|
568
|
-
.gate-btn { height: 44px; font-size: 13px; }
|
|
569
|
-
.inbox-row .actions .gate-btn { height: 44px; font-size: 13px; }
|
|
570
|
-
.gate-actions, .side-gate-actions, .inbox-row .actions { gap: 12px; }
|
|
571
|
-
}
|
|
577
|
+
|
|
572
578
|
.gate-approve { color: var(--status-review); border-color: rgba(22, 163, 74, 0.35); }
|
|
573
579
|
.gate-approve:hover { background: rgba(22, 163, 74, 0.08); border-color: var(--status-review); }
|
|
574
580
|
/* Agent runner (launch control) */
|
|
@@ -2241,8 +2247,16 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2241
2247
|
.sidebar-scrim.open { opacity: 1; pointer-events: auto; }
|
|
2242
2248
|
.menu-btn { display: inline-flex; }
|
|
2243
2249
|
.topbar { padding: 0 10px; gap: 8px; }
|
|
2250
|
+
/* Raising every control to 44px gave the actions 223px of a 375px bar and
|
|
2251
|
+
crushed the breadcrumb to "g… / B." — present, unreadable, worse than absent.
|
|
2252
|
+
The project name is already at the top of the drawer and the tab name is the
|
|
2253
|
+
row you tapped to get here, so the phone keeps only where you are. */
|
|
2244
2254
|
.topbar .crumbs { min-width: 0; overflow: hidden; white-space: nowrap; }
|
|
2245
2255
|
.topbar .crumbs > span { overflow: hidden; text-overflow: ellipsis; }
|
|
2256
|
+
.topbar .crumbs .proj-icon,
|
|
2257
|
+
.topbar .crumbs .sep,
|
|
2258
|
+
.topbar .crumbs #crumb-project { display: none; }
|
|
2259
|
+
.topbar .crumbs .here { color: var(--text); font-weight: 600; }
|
|
2246
2260
|
.topbar-actions { flex-shrink: 0; }
|
|
2247
2261
|
/* The search box is the one topbar item that can give up its width; ⌘K still
|
|
2248
2262
|
reaches it, and on a phone there is no ⌘K but there is the Search tab. */
|
|
@@ -2252,6 +2266,34 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2252
2266
|
/* Icon-only, with the name moved to aria-label/title rather than deleted. */
|
|
2253
2267
|
.btn-label { display: none; }
|
|
2254
2268
|
|
|
2269
|
+
/* The row's own arithmetic, measured rather than eyeballed: the page gives
|
|
2270
|
+
64px to its side padding and the row another 30px to border and padding,
|
|
2271
|
+
leaving 281px for the tracks. The fixed ones — id 110, two 14px gaps, and a
|
|
2272
|
+
44px Approve/Reject pair at about 156 — come to 294. The 1fr title column
|
|
2273
|
+
resolves to ZERO and the row overflows its own border box.
|
|
2274
|
+
Raising the buttons to a real touch target is what finished it off: at 24px
|
|
2275
|
+
they fit, at 44px they do not. Fixing one defect deepened its neighbour.
|
|
2276
|
+
So on a phone the row stops being three columns and becomes three rows:
|
|
2277
|
+
title first at full width, the id demoted to a meta line, the actions
|
|
2278
|
+
spanning the bottom. CSS only — the markup this renders from is shared with
|
|
2279
|
+
the desktop layout, and the desktop layout is fine. */
|
|
2280
|
+
.inbox-page { padding: 16px 16px 48px; }
|
|
2281
|
+
.inbox-row {
|
|
2282
|
+
display: flex; flex-wrap: wrap;
|
|
2283
|
+
gap: 8px 10px; align-items: baseline;
|
|
2284
|
+
}
|
|
2285
|
+
.inbox-row .ttl { order: 1; flex: 1 1 100%; font-size: 14px; }
|
|
2286
|
+
/* Two lines, wrapped — a phone has the vertical room it lacks sideways. */
|
|
2287
|
+
.inbox-row .ttl > :first-child { -webkit-line-clamp: 2; white-space: normal; }
|
|
2288
|
+
/* The id and the status share a line: stacked, each took a full row of its
|
|
2289
|
+
own and a four-line card said three things. */
|
|
2290
|
+
.inbox-row .id { order: 2; flex: 0 1 auto; padding-top: 0; }
|
|
2291
|
+
.inbox-row .actions { order: 3; margin-left: auto; }
|
|
2292
|
+
/* Except on a gate, where the line is a pair of 44px buttons and wants the
|
|
2293
|
+
whole width — the one row on this screen where the target size is the point. */
|
|
2294
|
+
.inbox-row .actions:has(.gate-btn) { flex: 1 0 100%; margin-left: 0; }
|
|
2295
|
+
.inbox-row .actions .gate-btn { flex: 1; }
|
|
2296
|
+
|
|
2255
2297
|
/* Four columns of numbers do not fit in 375px; they were being clipped, which
|
|
2256
2298
|
silently dropped the fourth stat rather than wrapping it. */
|
|
2257
2299
|
.inbox-summary { grid-template-columns: repeat(2, 1fr); }
|
|
@@ -2259,6 +2301,29 @@ button { font-family: inherit; cursor: pointer; }
|
|
|
2259
2301
|
.mp-hero { grid-template-columns: 1fr; }
|
|
2260
2302
|
.mp-row { grid-template-columns: 1fr; }
|
|
2261
2303
|
}
|
|
2304
|
+
@media (pointer: coarse) {
|
|
2305
|
+
/* A floor, not a list.
|
|
2306
|
+
Placed here, at the END of the stylesheet, for the same reason the
|
|
2307
|
+
phone-width block is: at equal specificity the later rule wins. Written
|
|
2308
|
+
higher up, its `.inbox-row .actions { gap: 12px }` lost to the base rule's
|
|
2309
|
+
4px declared 900 lines below it — so Approve and Reject sat 4px apart on a
|
|
2310
|
+
touch screen while a block that read as correct sat above saying 12.
|
|
2311
|
+
A declaration that loses is not a declaration.
|
|
2312
|
+
The first version of this block named .gate-btn and stopped there, because
|
|
2313
|
+
those were the buttons a review had pointed at. Two controls this same file
|
|
2314
|
+
had just added — the drawer's own hamburger at 40px and the topbar's icon
|
|
2315
|
+
buttons at ~32px — sat under the minimum from the day they were written,
|
|
2316
|
+
and nothing objected, because the rule was a list of what somebody had
|
|
2317
|
+
remembered rather than a property of the page.
|
|
2318
|
+
So: every control gets the floor, and the ones with something to say about
|
|
2319
|
+
their own size say it after. A button added tomorrow is covered by default;
|
|
2320
|
+
one that must stay small has to opt out in writing. */
|
|
2321
|
+
button, [role="button"], .nav-item, .toggle, summary { min-height: 44px; }
|
|
2322
|
+
.gate-btn, .inbox-row .actions .gate-btn { height: 44px; font-size: 13px; }
|
|
2323
|
+
.menu-btn { width: 44px; height: 44px; }
|
|
2324
|
+
.btn-bell { min-width: 44px; justify-content: center; }
|
|
2325
|
+
.gate-actions, .side-gate-actions, .inbox-row .actions { gap: 12px; }
|
|
2326
|
+
}
|
|
2262
2327
|
@keyframes pulse-glow { 0% { box-shadow: 0 0 0 0 rgba(76,175,80,.6);} 100% { box-shadow: 0 0 0 6px rgba(76,175,80,0);} }
|
|
2263
2328
|
/* ── Scrollbar ────────────────────────────────────────────────────────────── */
|
|
2264
2329
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
@@ -4509,6 +4574,7 @@ function connectSSE() {
|
|
|
4509
4574
|
// Sidebar counter FIRST, independent of render
|
|
4510
4575
|
updateNavCounts();
|
|
4511
4576
|
try { renderKanban(filterTasks(searchQuery)); } catch (e) { console.warn('SSE renderKanban failed', e); }
|
|
4577
|
+
checkBoardFreshness();
|
|
4512
4578
|
refreshInbox();
|
|
4513
4579
|
};
|
|
4514
4580
|
es.addEventListener('tasks', e => {
|
|
@@ -5397,6 +5463,33 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSide();
|
|
|
5397
5463
|
// Three things a drawer owes a keyboard, all of them things a transform alone
|
|
5398
5464
|
// does not give you: it leaves the tab order when closed, Escape closes it, and
|
|
5399
5465
|
// focus moves in on open and back to the button that opened it on close.
|
|
5466
|
+
/* The board tells on itself.
|
|
5467
|
+
*
|
|
5468
|
+
* A board is a process, and a process keeps the version it started with. This
|
|
5469
|
+
* one served v2.95.0 for nine days while three installs in a row reported
|
|
5470
|
+
* success — the installer wrote files, the running server kept answering, and
|
|
5471
|
+
* nothing compared the two. The installer restarts the board now; this is for a
|
|
5472
|
+
* board started any other way, so that being behind is visible rather than
|
|
5473
|
+
* merely true.
|
|
5474
|
+
*
|
|
5475
|
+
* Three states, like everywhere else on this page: `yes` says so, `no` is
|
|
5476
|
+
* silent, and `unknown` — the plugin cache could not be read — is also silent,
|
|
5477
|
+
* because a comparison we could not make must not be announced as a mismatch.
|
|
5478
|
+
*/
|
|
5479
|
+
async function checkBoardFreshness() {
|
|
5480
|
+
const v = await api('/api/version');
|
|
5481
|
+
if (!v || v.stale !== 'yes') return;
|
|
5482
|
+
const host = document.querySelector('.workspace');
|
|
5483
|
+
if (!host || document.getElementById('stale-board')) return;
|
|
5484
|
+
const el = document.createElement('div');
|
|
5485
|
+
el.id = 'stale-board';
|
|
5486
|
+
el.className = 'stale-board';
|
|
5487
|
+
el.setAttribute('role', 'status');
|
|
5488
|
+
el.innerHTML = `<span>This board is running <code>v${esc(v.version)}</code> — `
|
|
5489
|
+
+ `<code>v${esc(v.installed)}</code> is installed. Restart it to pick up the new version.</span>`;
|
|
5490
|
+
host.insertBefore(el, host.firstChild);
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5400
5493
|
function menuIsOpen() {
|
|
5401
5494
|
return !!document.getElementById('sidebar')?.classList.contains('open');
|
|
5402
5495
|
}
|
package/package.json
CHANGED