great-cto 2.98.1 → 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.98.1",
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
- const BD_CACHE_TTL_MS = 2000;
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({ version: BUILD_VERSION, surface: 'builder', node: process.version.replace(/^v/, '') }));
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;
@@ -4564,6 +4574,7 @@ function connectSSE() {
4564
4574
  // Sidebar counter FIRST, independent of render
4565
4575
  updateNavCounts();
4566
4576
  try { renderKanban(filterTasks(searchQuery)); } catch (e) { console.warn('SSE renderKanban failed', e); }
4577
+ checkBoardFreshness();
4567
4578
  refreshInbox();
4568
4579
  };
4569
4580
  es.addEventListener('tasks', e => {
@@ -5452,6 +5463,33 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSide();
5452
5463
  // Three things a drawer owes a keyboard, all of them things a transform alone
5453
5464
  // does not give you: it leaves the tab order when closed, Escape closes it, and
5454
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
+
5455
5493
  function menuIsOpen() {
5456
5494
  return !!document.getElementById('sidebar')?.classList.contains('open');
5457
5495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.98.1",
3
+ "version": "2.99.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",