great-cto 2.91.0 → 2.92.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.92.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Great CTO",
|
|
8
8
|
"url": "https://github.com/avelikiy/great_cto"
|
|
@@ -132,7 +132,14 @@ function tasksMdStatus(rawStatus, id, title) {
|
|
|
132
132
|
else if (s === 'in_progress' || s === 'in-progress' || s === 'wip' || s === 'doing') status = 'in_progress';
|
|
133
133
|
else if (s === 'blocked') status = 'blocked';
|
|
134
134
|
else status = isGate ? 'gate' : 'backlog';
|
|
135
|
-
|
|
135
|
+
// raw_status is what the inbox filters on, because mapStatus() rewrites any
|
|
136
|
+
// gate-labelled task to 'gate' and filtering on the mapped value would leave
|
|
137
|
+
// closed gates in the inbox forever. That fix covered `done`; it did not cover
|
|
138
|
+
// `blocked`, so a gate marked blocked in tasks.md reported raw_status 'open'
|
|
139
|
+
// and never left the inbox — for exactly the bd-less projects this parser
|
|
140
|
+
// exists to serve.
|
|
141
|
+
const raw = status === 'done' ? 'closed' : status === 'blocked' ? 'blocked' : 'open';
|
|
142
|
+
return { status, raw_status: raw, isGate };
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
// Build the full task record both parsers emit — one shape so getTasks can
|
|
@@ -23,7 +23,12 @@ function deriveDomain(slug) {
|
|
|
23
23
|
const s = slug.toLowerCase();
|
|
24
24
|
if (/architect|adr|design|prompt/.test(s)) return 'arch';
|
|
25
25
|
if (/security|sec-|threat|pci|gdpr|hipaa/.test(s)) return 'security';
|
|
26
|
-
|
|
26
|
+
// `review` used to be in this alternation, and it matched every `*-reviewer`
|
|
27
|
+
// slug — so 37 of the 40 agents in `qa` were domain reviewers, the `domain`
|
|
28
|
+
// bucket below was permanently empty, and the panel grouped 58% of the fleet
|
|
29
|
+
// under one heading. The rule that was supposed to separate them sat two lines
|
|
30
|
+
// later and could never be reached.
|
|
31
|
+
if (/\bqa\b|test|eval|^code-reviewer$/.test(s)) return 'qa';
|
|
27
32
|
if (/devops|deploy|infra|l3|support|oncall/.test(s)) return 'ops';
|
|
28
33
|
if (/reviewer$/.test(s)) return 'domain';
|
|
29
34
|
if (/pm|plan|product/.test(s)) return 'pm';
|
package/dist/main.js
CHANGED
|
@@ -1196,6 +1196,23 @@ async function runSelfUpgrade() {
|
|
|
1196
1196
|
return result.exitCode;
|
|
1197
1197
|
}
|
|
1198
1198
|
success(result.message);
|
|
1199
|
+
// The CLI is upgraded; the plugin Claude Code actually loads may not be.
|
|
1200
|
+
// `npm i -g` never touches the plugin cache, so without this the user is told
|
|
1201
|
+
// the upgrade succeeded and keeps running the previous release's hooks and
|
|
1202
|
+
// agent prompts with nothing anywhere saying so.
|
|
1203
|
+
try {
|
|
1204
|
+
const { pluginCacheLagWarning } = await import("./self-upgrade.js");
|
|
1205
|
+
const { readdirSync } = await import("node:fs");
|
|
1206
|
+
const { join } = await import("node:path");
|
|
1207
|
+
const cacheRoot = join(homedir(), ".claude", "plugins", "cache", "local", "great_cto");
|
|
1208
|
+
const dirs = readdirSync(cacheRoot);
|
|
1209
|
+
const warning = pluginCacheLagWarning(result.newVersion ?? "", dirs);
|
|
1210
|
+
if (warning) {
|
|
1211
|
+
log("");
|
|
1212
|
+
log(dim(warning));
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
catch { /* no plugin cache is not a lag — the plugin is simply not installed */ }
|
|
1199
1216
|
return 0;
|
|
1200
1217
|
}
|
|
1201
1218
|
async function runUpgrade(rawArgv, args) {
|
|
@@ -1242,6 +1259,10 @@ async function main() {
|
|
|
1242
1259
|
// is the single exit funnel: it fires one fire-and-forget event (command + node + os
|
|
1243
1260
|
// + exit_code + duration, no PII) only when the user has opted in, then exits.
|
|
1244
1261
|
const __tStart = Date.now();
|
|
1262
|
+
// Promise<never>, not Promise<void>: this always exits, and typing it as such
|
|
1263
|
+
// is what lets `await finish(2)` narrow a nullable the way `process.exit(2)`
|
|
1264
|
+
// did. Without it, routing an exit through the update check would silently
|
|
1265
|
+
// widen types at every call site.
|
|
1245
1266
|
const finish = async (code) => {
|
|
1246
1267
|
try {
|
|
1247
1268
|
await sendUsagePing({
|
|
@@ -1270,7 +1291,7 @@ async function main() {
|
|
|
1270
1291
|
if (args.command === "telemetry") {
|
|
1271
1292
|
const { exitCode, output } = telemetrySubcommand(args.positional[0]);
|
|
1272
1293
|
process.stdout.write(output);
|
|
1273
|
-
|
|
1294
|
+
await finish(exitCode);
|
|
1274
1295
|
}
|
|
1275
1296
|
if (args.command === "help") {
|
|
1276
1297
|
printHelp();
|
|
@@ -1281,7 +1302,7 @@ async function main() {
|
|
|
1281
1302
|
error(`great-cto: unknown command or flag '${tok}'`);
|
|
1282
1303
|
log("");
|
|
1283
1304
|
log(`Run ${cyan("great-cto --help")} for usage.`);
|
|
1284
|
-
|
|
1305
|
+
await finish(2);
|
|
1285
1306
|
}
|
|
1286
1307
|
if (args.command === "board") {
|
|
1287
1308
|
try {
|
|
@@ -1304,7 +1325,7 @@ async function main() {
|
|
|
1304
1325
|
}
|
|
1305
1326
|
catch (e) {
|
|
1306
1327
|
error(e.message);
|
|
1307
|
-
|
|
1328
|
+
await finish(1);
|
|
1308
1329
|
}
|
|
1309
1330
|
}
|
|
1310
1331
|
if (args.command === "console") {
|
|
@@ -1314,7 +1335,7 @@ async function main() {
|
|
|
1314
1335
|
}
|
|
1315
1336
|
catch (e) {
|
|
1316
1337
|
error(e.message);
|
|
1317
|
-
|
|
1338
|
+
await finish(1);
|
|
1318
1339
|
}
|
|
1319
1340
|
}
|
|
1320
1341
|
if (args.command === "register") {
|
|
@@ -1324,7 +1345,7 @@ async function main() {
|
|
|
1324
1345
|
}
|
|
1325
1346
|
catch (e) {
|
|
1326
1347
|
error(e.message);
|
|
1327
|
-
|
|
1348
|
+
await finish(1);
|
|
1328
1349
|
}
|
|
1329
1350
|
}
|
|
1330
1351
|
if (args.command === "ci") {
|
|
@@ -1335,7 +1356,7 @@ async function main() {
|
|
|
1335
1356
|
}
|
|
1336
1357
|
catch (e) {
|
|
1337
1358
|
error(e.message);
|
|
1338
|
-
|
|
1359
|
+
await finish(2);
|
|
1339
1360
|
}
|
|
1340
1361
|
}
|
|
1341
1362
|
if (args.command === "mcp") {
|
|
@@ -1349,7 +1370,7 @@ async function main() {
|
|
|
1349
1370
|
}
|
|
1350
1371
|
catch (e) {
|
|
1351
1372
|
error(e.message);
|
|
1352
|
-
|
|
1373
|
+
await finish(2);
|
|
1353
1374
|
}
|
|
1354
1375
|
}
|
|
1355
1376
|
if (args.command === "adapt") {
|
|
@@ -1364,16 +1385,16 @@ async function main() {
|
|
|
1364
1385
|
}
|
|
1365
1386
|
catch (e) {
|
|
1366
1387
|
error(e.message);
|
|
1367
|
-
|
|
1388
|
+
await finish(2);
|
|
1368
1389
|
}
|
|
1369
1390
|
}
|
|
1370
1391
|
if (args.command === "task") {
|
|
1371
1392
|
const { runTask } = await import("./worker.js");
|
|
1372
|
-
|
|
1393
|
+
await finish(await runTask(args.taskArgs ?? []));
|
|
1373
1394
|
}
|
|
1374
1395
|
if (args.command === "worker") {
|
|
1375
1396
|
const { runWorker } = await import("./worker.js");
|
|
1376
|
-
|
|
1397
|
+
await finish(await runWorker(args.taskArgs ?? []));
|
|
1377
1398
|
}
|
|
1378
1399
|
if (args.command === "serve") {
|
|
1379
1400
|
try {
|
|
@@ -1390,7 +1411,7 @@ async function main() {
|
|
|
1390
1411
|
}
|
|
1391
1412
|
catch (e) {
|
|
1392
1413
|
error(e.message);
|
|
1393
|
-
|
|
1414
|
+
await finish(2);
|
|
1394
1415
|
}
|
|
1395
1416
|
}
|
|
1396
1417
|
if (args.command === "webhook") {
|
|
@@ -1399,14 +1420,14 @@ async function main() {
|
|
|
1399
1420
|
const parsed = parseWebhookArgs(rawArgv);
|
|
1400
1421
|
if (!parsed) {
|
|
1401
1422
|
error("usage: great-cto webhook list | add-incoming <name> --secret <s> | add-outgoing <name> --url <u> --format <f> --triggers <t1,t2> | remove <name> | test <name>");
|
|
1402
|
-
|
|
1423
|
+
return finish(2);
|
|
1403
1424
|
}
|
|
1404
1425
|
const code = await runWebhookCli(parsed);
|
|
1405
1426
|
await finish(code);
|
|
1406
1427
|
}
|
|
1407
1428
|
catch (e) {
|
|
1408
1429
|
error(e.message);
|
|
1409
|
-
|
|
1430
|
+
await finish(2);
|
|
1410
1431
|
}
|
|
1411
1432
|
}
|
|
1412
1433
|
if (args.command === "upgrade") {
|
|
@@ -1416,7 +1437,7 @@ async function main() {
|
|
|
1416
1437
|
}
|
|
1417
1438
|
catch (e) {
|
|
1418
1439
|
error(e.message);
|
|
1419
|
-
|
|
1440
|
+
await finish(2);
|
|
1420
1441
|
}
|
|
1421
1442
|
}
|
|
1422
1443
|
if (args.command === "chat-only-hint") {
|
|
@@ -1433,21 +1454,21 @@ async function main() {
|
|
|
1433
1454
|
log(` ${cyan("serve")} · ${cyan("webhook")} · ${cyan("report")} · ${cyan("board")} · ${cyan("register")} · ${cyan("upgrade")}`);
|
|
1434
1455
|
log("");
|
|
1435
1456
|
log(`Run ${cyan("npx great-cto --help")} for the full CLI reference.`);
|
|
1436
|
-
|
|
1457
|
+
await finish(2);
|
|
1437
1458
|
}
|
|
1438
1459
|
if (args.command === "report") {
|
|
1439
1460
|
try {
|
|
1440
1461
|
const { runReport, parseReportArgs } = await import("./report.js");
|
|
1441
1462
|
const parsed = parseReportArgs(rawArgv, args.dir);
|
|
1442
1463
|
if (!parsed) {
|
|
1443
|
-
|
|
1464
|
+
return finish(2);
|
|
1444
1465
|
}
|
|
1445
1466
|
const code = await runReport(parsed);
|
|
1446
1467
|
await finish(code);
|
|
1447
1468
|
}
|
|
1448
1469
|
catch (e) {
|
|
1449
1470
|
error(e.message);
|
|
1450
|
-
|
|
1471
|
+
await finish(2);
|
|
1451
1472
|
}
|
|
1452
1473
|
}
|
|
1453
1474
|
if (args.command === "version") {
|
|
@@ -1464,7 +1485,7 @@ async function main() {
|
|
|
1464
1485
|
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
1465
1486
|
if (pkg.name === "great-cto" && pkg.version) {
|
|
1466
1487
|
log(pkg.version);
|
|
1467
|
-
|
|
1488
|
+
await finish(0);
|
|
1468
1489
|
}
|
|
1469
1490
|
}
|
|
1470
1491
|
catch { /* keep searching */ }
|
|
@@ -1474,7 +1495,7 @@ async function main() {
|
|
|
1474
1495
|
catch {
|
|
1475
1496
|
log("0.0.0");
|
|
1476
1497
|
}
|
|
1477
|
-
|
|
1498
|
+
await finish(0);
|
|
1478
1499
|
}
|
|
1479
1500
|
try {
|
|
1480
1501
|
const code = await runInit(args);
|
package/dist/self-upgrade.js
CHANGED
|
@@ -154,3 +154,57 @@ export function performSelfUpgrade(opts) {
|
|
|
154
154
|
}
|
|
155
155
|
// Re-export for callers that only need the prefix-derivation logic directly.
|
|
156
156
|
export { derivePrefixFromBinPath as _derivePrefixFromBinPath };
|
|
157
|
+
/**
|
|
158
|
+
* Newest version present in the Claude Code plugin cache, or null.
|
|
159
|
+
*
|
|
160
|
+
* Takes the directory names so the rule is testable without a filesystem.
|
|
161
|
+
*/
|
|
162
|
+
export function newestCachedPlugin(dirNames) {
|
|
163
|
+
const versions = dirNames.filter((d) => /^\d+\.\d+\.\d+$/.test(d));
|
|
164
|
+
if (!versions.length)
|
|
165
|
+
return null;
|
|
166
|
+
const key = (v) => v.split(".").map(Number);
|
|
167
|
+
return versions.sort((a, b) => {
|
|
168
|
+
const [A, B] = [key(a), key(b)];
|
|
169
|
+
for (let i = 0; i < 3; i++)
|
|
170
|
+
if (A[i] !== B[i])
|
|
171
|
+
return A[i] - B[i];
|
|
172
|
+
return 0;
|
|
173
|
+
})[versions.length - 1];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Warn when the npm package has moved ahead of the plugin Claude Code loads.
|
|
177
|
+
*
|
|
178
|
+
* `npm i -g` replaces the CLI. It does not touch
|
|
179
|
+
* ~/.claude/plugins/cache/local/great_cto/<version>/, which is where the hooks
|
|
180
|
+
* and agent prompts are actually read from — so a user can take the update
|
|
181
|
+
* prompt, be told the upgrade succeeded, and keep running the previous release's
|
|
182
|
+
* agents with nothing anywhere saying so. That is the same defect this codebase
|
|
183
|
+
* keeps finding: a step reported as done while the thing it was meant to change
|
|
184
|
+
* stayed as it was.
|
|
185
|
+
*
|
|
186
|
+
* Returns null when they match, when nothing is cached (the plugin is simply not
|
|
187
|
+
* installed), or when the cache is somehow ahead — none of those are the failure
|
|
188
|
+
* this exists to catch.
|
|
189
|
+
*/
|
|
190
|
+
export function pluginCacheLagWarning(npmVersion, cachedDirs) {
|
|
191
|
+
const cached = newestCachedPlugin(cachedDirs);
|
|
192
|
+
if (!cached || !npmVersion)
|
|
193
|
+
return null;
|
|
194
|
+
if (cached === npmVersion)
|
|
195
|
+
return null;
|
|
196
|
+
const key = (v) => v.split(".").map(Number);
|
|
197
|
+
const [c, n] = [key(cached), key(npmVersion)];
|
|
198
|
+
for (let i = 0; i < 3; i++) {
|
|
199
|
+
if (c[i] > n[i])
|
|
200
|
+
return null; // cache ahead — not the case we guard
|
|
201
|
+
if (c[i] < n[i])
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
`note: the npm package is ${npmVersion}, but the plugin Claude Code loads is ${cached}.`,
|
|
206
|
+
" Hooks and agent prompts come from the plugin cache, not from npm — until it is",
|
|
207
|
+
" refreshed, this session keeps running the previous release's agents.",
|
|
208
|
+
" Refresh it with: bash scripts/install-local.sh --prune",
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
package/dist/update-check.js
CHANGED
|
@@ -58,6 +58,29 @@ export function cachePath() {
|
|
|
58
58
|
}
|
|
59
59
|
/** Pure function: is a suppression condition active? Fail-open to "suppressed" on any ambiguity. */
|
|
60
60
|
export function isSuppressed(opts = {}) {
|
|
61
|
+
if (isRefreshSuppressed(opts))
|
|
62
|
+
return true;
|
|
63
|
+
// Nothing may be printed when stderr is not a terminal — the output is being
|
|
64
|
+
// read by something. This is about PRINTING only; see isRefreshSuppressed.
|
|
65
|
+
if (opts.stderrIsTTY === false)
|
|
66
|
+
return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Should the background cache refresh be suppressed too?
|
|
71
|
+
*
|
|
72
|
+
* Deliberately narrower than isSuppressed. Not printing and not learning are
|
|
73
|
+
* different things, and conflating them had a consequence: the refresh sits
|
|
74
|
+
* behind the same non-TTY check as the hint, so a user whose runs are mostly
|
|
75
|
+
* non-interactive never warmed the cache — and their occasional interactive run
|
|
76
|
+
* therefore had no cache to read and showed nothing, every time. The cache file
|
|
77
|
+
* was simply absent on this machine, which is how this was found.
|
|
78
|
+
*
|
|
79
|
+
* A detached refresh with stdio ignored pollutes no output. What genuinely must
|
|
80
|
+
* not spawn: the protocol commands, whose children would outlive a parsed
|
|
81
|
+
* stream, and anyone who asked us not to check at all.
|
|
82
|
+
*/
|
|
83
|
+
export function isRefreshSuppressed(opts = {}) {
|
|
61
84
|
const env = opts.env ?? process.env;
|
|
62
85
|
if (env.CI != null && env.CI !== "" && env.CI !== "0" && env.CI !== "false")
|
|
63
86
|
return true;
|
|
@@ -65,8 +88,6 @@ export function isSuppressed(opts = {}) {
|
|
|
65
88
|
return true;
|
|
66
89
|
if (opts.command && PROTOCOL_SENSITIVE_COMMANDS.has(opts.command))
|
|
67
90
|
return true;
|
|
68
|
-
if (opts.stderrIsTTY === false)
|
|
69
|
-
return true;
|
|
70
91
|
return false;
|
|
71
92
|
}
|
|
72
93
|
/**
|
|
@@ -213,10 +234,15 @@ export async function checkForUpdate(opts) {
|
|
|
213
234
|
const env = opts.env ?? process.env;
|
|
214
235
|
const stderrIsTTY = opts.stderrIsTTY ?? Boolean(process.stderr.isTTY);
|
|
215
236
|
const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
216
|
-
|
|
237
|
+
// Warm the cache even when nothing may be printed. A run that cannot show a
|
|
238
|
+
// hint can still learn the version for the run that can.
|
|
239
|
+
if (isRefreshSuppressed({ env, command: opts.command }))
|
|
217
240
|
return;
|
|
241
|
+
const quiet = isSuppressed({ env, command: opts.command, stderrIsTTY });
|
|
218
242
|
const cache = readCache();
|
|
219
243
|
if (isCacheFresh(cache, opts.now)) {
|
|
244
|
+
if (quiet)
|
|
245
|
+
return; // fresh cache, but this run may not speak
|
|
220
246
|
if (!cache || !isNewerVersion(opts.currentVersion, cache.latest))
|
|
221
247
|
return;
|
|
222
248
|
if (shouldPrompt({ currentVersion: opts.currentVersion, cache, env, command: opts.command, stderrIsTTY, stdinIsTTY })) {
|
package/package.json
CHANGED