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.
|
|
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
package/postinstall.mjs
CHANGED
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* great-cto postinstall —
|
|
4
|
-
*
|
|
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
|
+
}
|