great-cto 3.14.0 → 3.15.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": "3.14.0",
5
+ "version": "3.15.0",
6
6
  "author": {
7
7
  "name": "Great CTO",
8
8
  "url": "https://github.com/avelikiy/great_cto"
@@ -1822,6 +1822,39 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
1822
1822
  font-size: var(--fs-body);
1823
1823
  }
1824
1824
  .judge-status[hidden] { display: none; }
1825
+
1826
+ /* What the judge CONCLUDED, under whether it is connected.
1827
+ The scores existed for weeks and no page read them: /api/scores had zero
1828
+ callers, so every verification ran, wrote a record, and vanished. */
1829
+ .jscores { margin-bottom: 16px; }
1830
+ .jscores[hidden] { display: none; }
1831
+ .jscores-head {
1832
+ display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
1833
+ padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px 8px 0 0;
1834
+ background: var(--bg-muted); font-size: var(--fs-body);
1835
+ }
1836
+ .jscores-head .rate { font-size: var(--fs-num-s); font-weight: 600; }
1837
+ .jscores-rows { border: 1px solid var(--border); border-top: 0; border-radius: 0 0 8px 8px; }
1838
+ .jscore {
1839
+ display: grid; grid-template-columns: minmax(9rem, 12rem) 7.5rem 1fr auto;
1840
+ gap: 12px; align-items: baseline;
1841
+ padding: 7px 12px; border-top: 1px solid var(--border); font-size: var(--fs-small);
1842
+ }
1843
+ .jscore:first-child { border-top: 0; }
1844
+ .jscore .who { font-weight: 500; }
1845
+ .jscore .why { color: var(--text3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1846
+ .jscore .when { color: var(--text3); font-family: var(--mono); font-size: var(--fs-caption); }
1847
+ .jstate { font-weight: 600; }
1848
+ .jstate.verified { color: var(--status-done); }
1849
+ .jstate.rework { color: var(--status-progress); }
1850
+ /* Deliberately grey, not red. `unverifiable` is not a failing grade — it is the
1851
+ absence of one, and colouring it like a failure is the same collapse of three
1852
+ states into two that the scores exist to prevent. */
1853
+ .jstate.unverifiable { color: var(--text3); }
1854
+ @media (max-width: 640px) {
1855
+ .jscore { grid-template-columns: 1fr auto; }
1856
+ .jscore .why, .jscore .when { grid-column: 1 / -1; }
1857
+ }
1825
1858
  .judge-status .js-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
1826
1859
  .judge-status.ok .js-dot { background: var(--status-done); }
1827
1860
  .judge-status.off .js-dot { background: var(--status-blocked); }
@@ -3136,6 +3169,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
3136
3169
  key is not discoverable: the README names OpenRouter once without
3137
3170
  saying where it goes. -->
3138
3171
  <div id="judge-status" class="judge-status" hidden></div>
3172
+ <!-- The verdicts of the verifier itself. Kept beside the connection
3173
+ status because the two answer consecutive questions: is anything
3174
+ checking, and what did it find. -->
3175
+ <div id="judge-scores" class="jscores" hidden></div>
3139
3176
  <div id="budgets-page-body">loading…</div>
3140
3177
  </div>
3141
3178
  </div>
@@ -3960,6 +3997,75 @@ async function loadJudgeStatus() {
3960
3997
  </span>`;
3961
3998
  }
3962
3999
 
4000
+ /**
4001
+ * What the verifier concluded, per stage.
4002
+ *
4003
+ * `/api/scores` existed with ZERO front-end callers: every verification ran,
4004
+ * appended a record, and was never shown to anyone. The user's question — "how
4005
+ * much did introducing the judge improve anything, and why can I not see it?" —
4006
+ * had a correct answer of "you cannot, nothing renders it".
4007
+ *
4008
+ * The rate divides by ASSESSED, never by total, and says how many that was.
4009
+ * An agent with nine unverifiable runs and one verified run scores 100% here
4010
+ * with `1 assessed` beside it; reporting 10% would be a made-up number about
4011
+ * work nobody looked at.
4012
+ */
4013
+ async function loadJudgeScores() {
4014
+ const el = document.getElementById('judge-scores');
4015
+ if (!el) return;
4016
+ let d;
4017
+ try { d = await api(`/api/scores${pqs()}&limit=40`); } catch { d = null; }
4018
+
4019
+ // Three states, and the empty one earns a sentence rather than a blank space:
4020
+ // "nothing has been verified yet" and "the panel is broken" look identical
4021
+ // when a component renders nothing.
4022
+ if (!d || !Array.isArray(d.scores)) {
4023
+ el.hidden = false;
4024
+ el.innerHTML = `<div class="jscores-head"><span class="muted">Verification results could not be read.</span></div>`;
4025
+ return;
4026
+ }
4027
+ if (!d.scores.length) {
4028
+ el.hidden = false;
4029
+ el.innerHTML = `<div class="jscores-head"><span class="muted">No stage has been verified yet — run a pipeline and the judge's conclusions land here.</span></div>`;
4030
+ return;
4031
+ }
4032
+
4033
+ const sum = d.summary || {};
4034
+ const by = sum.byState || {};
4035
+ const rate = sum.rate === null || sum.rate === undefined ? '—' : `${sum.rate}%`;
4036
+
4037
+ // Newest first, one row per agent: an older score for the same stage is
4038
+ // history, and history belongs in the file, not in a status panel.
4039
+ const seen = new Set();
4040
+ const rows = d.scores.filter((s) => {
4041
+ if (seen.has(s.agent)) return false;
4042
+ seen.add(s.agent);
4043
+ return true;
4044
+ });
4045
+
4046
+ el.hidden = false;
4047
+ el.innerHTML = `
4048
+ <div class="jscores-head">
4049
+ <span class="rate">${rate}</span>
4050
+ <span class="muted">verified, of ${sum.assessed || 0} assessed</span>
4051
+ <span class="jstate verified">${by.verified || 0} verified</span>
4052
+ <span class="jstate rework">${by.rework || 0} rework</span>
4053
+ <span class="jstate unverifiable"
4054
+ title="Nothing was checked — not a failing grade. Excluded from the rate rather than counted as zeros, which is why the rate says how many it divided by.">${by.unverifiable || 0} unverifiable</span>
4055
+ ${d.unreadable_lines
4056
+ ? `<span class="jstate rework">${d.unreadable_lines} unreadable line(s)</span>` : ''}
4057
+ </div>
4058
+ <div class="jscores-rows">
4059
+ ${rows.map((s) => `
4060
+ <div class="jscore">
4061
+ <span class="who">${esc(s.agent)}</span>
4062
+ <span class="jstate ${esc(s.state)}">${esc(s.state)}</span>
4063
+ <span class="why" title="${esc(s.comment || '')}">${esc(s.comment || '')}</span>
4064
+ <span class="when">${esc(String(s.ts || '').slice(0, 10))} · ${esc(s.scorer || '')}</span>
4065
+ </div>`).join('')}
4066
+ </div>`;
4067
+ }
4068
+
3963
4069
  async function saveJudgeKey() {
3964
4070
  const input = document.getElementById('judge-key');
3965
4071
  const key = (input?.value || '').trim();
@@ -3982,6 +4088,7 @@ async function saveJudgeKey() {
3982
4088
 
3983
4089
  async function loadBudgetsPage() {
3984
4090
  loadJudgeStatus();
4091
+ loadJudgeScores();
3985
4092
  const body = document.getElementById('budgets-page-body');
3986
4093
  if (!body) return;
3987
4094
  body.innerHTML = '<div class="muted">loading…</div>';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.14.0",
3
+ "version": "3.15.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",
package/postinstall.mjs CHANGED
@@ -1,5 +1,124 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * great-cto postinstall — no-op placeholder.
4
- * Reserved for future setup steps; currently does nothing.
3
+ * great-cto postinstall — says when the plugin is now older than the CLI.
4
+ *
5
+ * THE PROBLEM
6
+ * -----------
7
+ * great-cto ships as two things on purpose: an npm package (the `great-cto`
8
+ * command) and a Claude Code plugin (agents, skills, commands, the board). They
9
+ * update through different channels — `npm i -g great-cto` moves one of them and
10
+ * nothing at all moves the other.
11
+ *
12
+ * So a user upgrades, sees a new version number, and runs a pipeline whose
13
+ * agents are three releases behind. Nothing errors. The board opened, the agents
14
+ * ran, the verdicts were written — by the old code. That is this project's
15
+ * governing defect in its own installer: a thing that did not happen looking
16
+ * exactly like a thing that did.
17
+ *
18
+ * WHAT THIS DELIBERATELY DOES NOT DO
19
+ * ----------------------------------
20
+ * It does not update the plugin. An npm lifecycle hook writing into
21
+ * ~/.claude/plugins/ would reach across a project boundary into global state
22
+ * that every other project's agents read, from a script the user never chose to
23
+ * run — the ADR-009 case that wants a human decision, not a silent one. `npm i`
24
+ * is not consent to rewrite the agent definitions of every project on the
25
+ * machine.
26
+ *
27
+ * So it detects and it tells. The user runs one command, or does not.
28
+ *
29
+ * IT ALSO MUST NOT BREAK AN INSTALL
30
+ * ---------------------------------
31
+ * A postinstall hook that throws fails `npm i`. Every path here is wrapped, the
32
+ * exit code is always 0, and every unknown answers "say nothing": no plugin
33
+ * directory is a perfectly good CLI-only install, and an unreadable one is not
34
+ * evidence of staleness.
5
35
  */
36
+
37
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
38
+ import { join, dirname } from 'node:path';
39
+ import { homedir } from 'node:os';
40
+ import { fileURLToPath } from 'node:url';
41
+
42
+ /** Numeric semver compare. Returns >0 when a is newer. Non-numeric parts sort last. */
43
+ export function cmpVersion(a, b) {
44
+ const parse = (v) => String(v).split('.').map((n) => (/^\d+$/.test(n) ? Number(n) : -1));
45
+ const x = parse(a);
46
+ const y = parse(b);
47
+ for (let i = 0; i < Math.max(x.length, y.length); i += 1) {
48
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
49
+ if (d) return d;
50
+ }
51
+ return 0;
52
+ }
53
+
54
+ /**
55
+ * Newest great_cto version across EVERY marketplace, not just `local`.
56
+ *
57
+ * Same reasoning as board-path: a user who installed the plugin from a
58
+ * marketplace of their own is not running a broken setup, and looking in one
59
+ * hard-coded directory would report them as having no plugin at all.
60
+ */
61
+ export function newestPluginVersion(cacheRoot) {
62
+ let best = null;
63
+ let dirs;
64
+ try { dirs = readdirSync(cacheRoot); } catch { return null; }
65
+ for (const marketplace of dirs) {
66
+ const base = join(cacheRoot, marketplace, 'great_cto');
67
+ let versions;
68
+ try { versions = readdirSync(base); } catch { continue; }
69
+ for (const v of versions) {
70
+ if (!/^\d/.test(v)) continue;
71
+ if (best === null || cmpVersion(v, best) > 0) best = v;
72
+ }
73
+ }
74
+ return best;
75
+ }
76
+
77
+ /**
78
+ * @returns {{state: 'stale'|'current'|'no-plugin', cli: string, plugin: string|null}}
79
+ *
80
+ * Three states, and `no-plugin` is not a problem to report. Someone who wants
81
+ * only the CLI has a correct installation, and telling them to update a plugin
82
+ * they never installed is the false alarm that teaches people to ignore output.
83
+ */
84
+ export function compare({ cli, plugin }) {
85
+ if (!plugin) return { state: 'no-plugin', cli, plugin: null };
86
+ return { state: cmpVersion(cli, plugin) > 0 ? 'stale' : 'current', cli, plugin };
87
+ }
88
+
89
+ export function message({ cli, plugin }) {
90
+ return [
91
+ '',
92
+ ` great-cto ${cli} is installed, but the Claude Code plugin is still ${plugin}.`,
93
+ '',
94
+ ' They update separately. Until the plugin is updated, your agents, skills',
95
+ ' and commands are the ones from ' + plugin + ' — pipelines will run, and they',
96
+ ' will run the old definitions.',
97
+ '',
98
+ ' In Claude Code: /plugin update great_cto',
99
+ '',
100
+ ].join('\n');
101
+ }
102
+
103
+ function main() {
104
+ // CI installs nobody reads, and a notice there is noise in a build log.
105
+ if (process.env.CI || process.env.GREAT_CTO_QUIET_POSTINSTALL) return;
106
+
107
+ let cli;
108
+ try {
109
+ const here = dirname(fileURLToPath(import.meta.url));
110
+ cli = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8')).version;
111
+ } catch { return; }
112
+ if (!cli) return;
113
+
114
+ const cacheRoot = join(homedir(), '.claude', 'plugins', 'cache');
115
+ if (!existsSync(cacheRoot)) return; // not a Claude Code machine — nothing to say
116
+
117
+ const r = compare({ cli, plugin: newestPluginVersion(cacheRoot) });
118
+ if (r.state === 'stale') process.stdout.write(message(r) + '\n');
119
+ }
120
+
121
+ // Only when run as the hook, so the pure parts above stay importable by tests.
122
+ if (process.argv[1] && process.argv[1].endsWith('postinstall.mjs')) {
123
+ try { main(); } catch { /* an installer must never fail the install */ }
124
+ }