great-cto 2.7.0 → 2.8.1

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.
@@ -180,6 +180,17 @@ const RULES = [
180
180
  const stackJoined = d.stack.join(" ").toLowerCase();
181
181
  if (/(?:^| )agent[-_](?:runtime|product|loop|kit|sdk)/.test(stackJoined))
182
182
  s += 2;
183
+ // Voice-AI agent: telephony provider + STT + TTS + LLM = autonomous voice agent
184
+ const voiceProviders = ["twilio", "vonage", "livekit"];
185
+ const sttProviders = ["deepgram"];
186
+ const ttsProviders = ["elevenlabs", "hume"];
187
+ const hasVoice = voiceProviders.some((s) => d.stack.includes(s));
188
+ const hasStt = sttProviders.some((s) => d.stack.includes(s));
189
+ const hasTts = ttsProviders.some((s) => d.stack.includes(s));
190
+ if (hasVoice && hasLlm && (hasStt || hasTts))
191
+ s += 7; // strong: full voice-agent stack
192
+ else if (hasVoice && hasLlm)
193
+ s += 4; // medium: voice + LLM
183
194
  return s;
184
195
  },
185
196
  reason: (d) => {
@@ -197,7 +208,10 @@ const RULES = [
197
208
  const vdb = ["pinecone", "weaviate", "chroma", "qdrant"].filter((s) => d.stack.includes(s));
198
209
  if (vdb.length)
199
210
  bits.push(`vector DB (${vdb.join(",")})`);
200
- return `agent-product detected ${bits.join(", ")} agent-eval + isolation + prompt-injection gates required`;
211
+ const voice = ["twilio", "vonage", "livekit", "deepgram", "elevenlabs", "hume"].filter((s) => d.stack.includes(s));
212
+ if (voice.length)
213
+ bits.push(`voice stack (${voice.join(",")})`);
214
+ return `agent-product detected — ${bits.join(", ") || "agent signals"} — agent-eval + isolation + prompt-injection gates required`;
201
215
  },
202
216
  },
203
217
  // ── ai-system ────────────────────────────────────
@@ -285,6 +299,15 @@ const RULES = [
285
299
  s += 9;
286
300
  if (d.stack.includes("fintech"))
287
301
  s += 7;
302
+ // Emerging-markets payment providers
303
+ if (d.stack.includes("razorpay"))
304
+ s += 9;
305
+ if (d.stack.includes("paystack"))
306
+ s += 9;
307
+ if (d.stack.includes("flutterwave"))
308
+ s += 9;
309
+ if (d.stack.includes("mercadopago"))
310
+ s += 9;
288
311
  if (d.readmeKeywords.includes("fintech"))
289
312
  s += 2;
290
313
  return s;
@@ -299,6 +322,14 @@ const RULES = [
299
322
  bits.push("Dwolla");
300
323
  if (d.stack.includes("teller"))
301
324
  bits.push("Teller");
325
+ if (d.stack.includes("razorpay"))
326
+ bits.push("Razorpay (India)");
327
+ if (d.stack.includes("paystack"))
328
+ bits.push("Paystack (Nigeria)");
329
+ if (d.stack.includes("flutterwave"))
330
+ bits.push("Flutterwave (Africa)");
331
+ if (d.stack.includes("mercadopago"))
332
+ bits.push("MercadoPago (LATAM)");
302
333
  return `fintech integration: ${bits.join(", ")} — SOX, PCI, KYC/AML compliance gates`;
303
334
  },
304
335
  },
@@ -1196,3 +1227,100 @@ export function suggestCompliance(d, archetype) {
1196
1227
  c.add("sox");
1197
1228
  return Array.from(c).sort();
1198
1229
  }
1230
+ /**
1231
+ * Which review agents fire for each archetype. The reviewer name maps to
1232
+ * `agents/<name>.md` in the great_cto plugin. Naming aliases exist for
1233
+ * historical reasons (pci-reviewer covers fintech, firmware-reviewer
1234
+ * covers iot-embedded, etc.) — documented in
1235
+ * docs/agents/REVIEWER-NAMING.md (gap A2 closure).
1236
+ *
1237
+ * `greenfield` has no reviewers — pipeline runs in nano mode only.
1238
+ */
1239
+ export const REVIEWERS_BY_ARCHETYPE = {
1240
+ "web-service": ["security-officer"],
1241
+ "mobile-app": ["mobile-store-reviewer", "security-officer"],
1242
+ "ai-system": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
1243
+ "agent-product": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
1244
+ "mlops": ["mlops-reviewer", "ai-security-reviewer"],
1245
+ "data-platform": ["data-platform-reviewer"],
1246
+ "streaming": ["streaming-reviewer"],
1247
+ "infra": ["infra-reviewer"],
1248
+ "library": ["library-reviewer"],
1249
+ "cli-tool": ["cli-reviewer"],
1250
+ "commerce": ["pci-reviewer", "security-officer"],
1251
+ "marketplace": ["marketplace-reviewer", "pci-reviewer"],
1252
+ "fintech": ["pci-reviewer", "regulated-reviewer"],
1253
+ "healthcare": ["healthcare-reviewer", "security-officer"],
1254
+ "web3": ["oracle-reviewer"],
1255
+ "iot-embedded": ["firmware-reviewer"],
1256
+ "regulated": ["regulated-reviewer"],
1257
+ "devtools": ["devtools-reviewer"],
1258
+ "browser-extension": ["web-store-reviewer"],
1259
+ "game": ["game-reviewer"],
1260
+ "cms": ["cms-reviewer"],
1261
+ "enterprise-saas": ["enterprise-saas-reviewer"],
1262
+ "edtech": ["edtech-reviewer"],
1263
+ "gov-public": ["gov-reviewer", "security-officer"],
1264
+ "insurance": ["insurance-reviewer", "regulated-reviewer"],
1265
+ "greenfield": [],
1266
+ };
1267
+ /**
1268
+ * Which human gates the pipeline opens for each archetype at the medium
1269
+ * project_size. Smaller sizes skip gates (see `gatesFor()` below); larger
1270
+ * sizes may add compliance.
1271
+ *
1272
+ * NOTE: nano → only [plan]. Enterprise → always adds compliance.
1273
+ */
1274
+ export const GATES_BY_ARCHETYPE = {
1275
+ "web-service": ["plan", "qa", "ship"],
1276
+ "mobile-app": ["plan", "qa", "ship"],
1277
+ "ai-system": ["plan", "cost", "qa", "security", "ship"],
1278
+ "agent-product": ["plan", "cost", "qa", "security", "ship"],
1279
+ "mlops": ["plan", "cost", "qa", "security", "ship"],
1280
+ "data-platform": ["plan", "qa", "ship"],
1281
+ "streaming": ["plan", "qa", "ship"],
1282
+ "infra": ["plan", "qa", "ship"],
1283
+ "library": ["plan", "qa", "ship"],
1284
+ "cli-tool": ["plan", "qa", "ship"],
1285
+ "commerce": ["plan", "qa", "security", "ship", "compliance"],
1286
+ "marketplace": ["plan", "qa", "security", "ship", "compliance"],
1287
+ "fintech": ["plan", "qa", "security", "ship", "compliance"],
1288
+ "healthcare": ["plan", "qa", "security", "ship", "compliance"],
1289
+ "web3": ["plan", "qa", "oracle-review", "security", "ship"],
1290
+ "iot-embedded": ["plan", "qa", "security", "ship"],
1291
+ "regulated": ["plan", "qa", "security", "ship", "compliance"],
1292
+ "devtools": ["plan", "qa", "ship"],
1293
+ "browser-extension": ["plan", "qa", "ship"],
1294
+ "game": ["plan", "qa", "ship"],
1295
+ "cms": ["plan", "qa", "ship"],
1296
+ "enterprise-saas": ["plan", "qa", "security", "ship", "compliance"],
1297
+ "edtech": ["plan", "qa", "edtech-review", "security", "ship", "compliance"],
1298
+ "gov-public": ["plan", "qa", "gov-review", "security", "ship", "compliance"],
1299
+ "insurance": ["plan", "qa", "insurance-review", "security", "ship", "compliance"],
1300
+ "greenfield": ["plan"],
1301
+ };
1302
+ /**
1303
+ * Returns the subset of gates the pipeline will actually open for a
1304
+ * given archetype + project_size. Used by the orchestrator before
1305
+ * opening any gate to skip unnecessary human checkpoints on smaller
1306
+ * projects (e.g. nano always skips QA).
1307
+ */
1308
+ export function gatesFor(archetype, size) {
1309
+ const all = GATES_BY_ARCHETYPE[archetype] ?? [];
1310
+ if (size === "nano")
1311
+ return all.filter((g) => g === "plan");
1312
+ if (size === "small")
1313
+ return all.filter((g) => g === "plan" || g === "ship");
1314
+ if (size === "medium")
1315
+ return all;
1316
+ // large + enterprise → ensure compliance is included
1317
+ return all.includes("compliance") ? all : [...all, "compliance"];
1318
+ }
1319
+ /**
1320
+ * Returns the ordered list of reviewers for an archetype. Empty for
1321
+ * `greenfield`. Used by the orchestrator to spawn the right
1322
+ * archetype-specific review stages after senior-dev.
1323
+ */
1324
+ export function reviewersFor(archetype) {
1325
+ return REVIEWERS_BY_ARCHETYPE[archetype] ?? [];
1326
+ }
package/dist/detect.js CHANGED
@@ -188,6 +188,23 @@ export function detect(dir) {
188
188
  stack.add("teller");
189
189
  sig("fintech", "teller");
190
190
  }
191
+ // Emerging-markets payment providers (Wave-1 pack signals)
192
+ if (has("razorpay") || has("razorpay-node")) {
193
+ stack.add("razorpay");
194
+ sig("fintech", "razorpay");
195
+ }
196
+ if (has("@paystackhq/paystack-node") || has("paystack")) {
197
+ stack.add("paystack");
198
+ sig("fintech", "paystack");
199
+ }
200
+ if (has("@flutterwave/flutterwave-node-v3") || has("flutterwave")) {
201
+ stack.add("flutterwave");
202
+ sig("fintech", "flutterwave");
203
+ }
204
+ if (has("mercadopago")) {
205
+ stack.add("mercadopago");
206
+ sig("fintech", "mercadopago");
207
+ }
191
208
  // Stripe Connect/Issuing → fintech (not just commerce)
192
209
  if (has("stripe") && (pkg.name?.includes("bank") || pkg.name?.includes("card"))) {
193
210
  sig("fintech", "stripe-connect");
@@ -206,6 +223,59 @@ export function detect(dir) {
206
223
  stack.add("hl7");
207
224
  sig("healthcare", "hl7");
208
225
  }
226
+ if (has("dicom-parser") || has("cornerstone-core") || has("dcmjs")) {
227
+ stack.add("dicom");
228
+ sig("healthcare", "dicom");
229
+ }
230
+ // Voice / telephony (Wave-1 pack signals)
231
+ if (has("twilio") || has("@twilio/voice-sdk")) {
232
+ stack.add("twilio");
233
+ sig("voice", "twilio");
234
+ }
235
+ if (has("@vonage/server-sdk") || has("nexmo")) {
236
+ stack.add("vonage");
237
+ sig("voice", "vonage");
238
+ }
239
+ if (has("livekit-server-sdk") || has("livekit-client")) {
240
+ stack.add("livekit");
241
+ sig("voice", "livekit");
242
+ }
243
+ if (has("@deepgram/sdk")) {
244
+ stack.add("deepgram");
245
+ sig("voice", "deepgram");
246
+ }
247
+ if (has("elevenlabs") || has("@elevenlabs/elevenlabs-js")) {
248
+ stack.add("elevenlabs");
249
+ sig("voice", "elevenlabs");
250
+ }
251
+ if (has("hume") || has("hume-ai")) {
252
+ stack.add("hume");
253
+ sig("voice", "hume");
254
+ }
255
+ // HR / recruiting (Wave-1 pack signals)
256
+ if (has("greenhouse-io") || has("@greenhouse/api")) {
257
+ stack.add("greenhouse");
258
+ sig("hr", "greenhouse");
259
+ }
260
+ if (has("lever-api")) {
261
+ stack.add("lever");
262
+ sig("hr", "lever");
263
+ }
264
+ if (has("ashby-api")) {
265
+ stack.add("ashby");
266
+ sig("hr", "ashby");
267
+ }
268
+ // API platform (Wave-1 pack signals)
269
+ if (has("fastify"))
270
+ stack.add("fastify");
271
+ if (has("@trpc/server") || has("@trpc/client"))
272
+ stack.add("trpc");
273
+ if (has("@apollo/server") || has("apollo-server"))
274
+ stack.add("graphql");
275
+ if (has("graphql") || has("graphql-yoga"))
276
+ stack.add("graphql");
277
+ if (has("openapi3-ts") || has("@apidevtools/swagger-parser"))
278
+ stack.add("openapi");
209
279
  // Auth
210
280
  if (has("next-auth") || has("@auth/core"))
211
281
  stack.add("auth");
@@ -1043,6 +1113,56 @@ function mineReadmeKeywords(dir) {
1043
1113
  if (terms.some((t) => text.includes(t)))
1044
1114
  kws.add(bucket);
1045
1115
  }
1116
+ // Wave 1-3 pack-trigger raw terms — emitted verbatim so packs.ts
1117
+ // can substring-match them. Keep in sync with packs.ts SIGNALS.keywords.
1118
+ // Single tokens + multi-word phrases supported.
1119
+ const packTerms = [
1120
+ // voice-pack
1121
+ "voice", "telephony", "ivr", "tts", "stt", "speech-to-text", "text-to-speech",
1122
+ "outbound call", "inbound call", "voice agent",
1123
+ // clinical-pack + clinical-trials-pack
1124
+ "clinical", "patient", "ehr", "emr", "phi", "diagnosis", "diagnos", "triage",
1125
+ "radiolog", "patholog", "samd", "scribe", "telehealth-ai", "medical record",
1126
+ "cds", "clinical decision support",
1127
+ "clinical trial", "ctms", "edc", "epro", "econsent", "esource",
1128
+ "randomization", "rtsm", "irt", "decentralized trial", "ind submission",
1129
+ "21 cfr 11", "cdisc", "sdtm", "adam", "irb",
1130
+ // hr-ai-pack
1131
+ "recruit", "hiring", "candidate", "resume", "interview", "ats", "talent acquisition",
1132
+ "performance review", "workforce scheduling", "employee evaluation", "aedt",
1133
+ // api-platform-pack
1134
+ "public api", "partner api", "developer portal", "api key", "webhook", "sdk",
1135
+ "rest api", "graphql api", "openapi",
1136
+ // lending-pack
1137
+ "loan", "lending", "credit decision", "underwrit", "bnpl", "buy now pay later",
1138
+ "buy-now-pay-later", "payroll advance", "ewa", "line of credit", "fico",
1139
+ "credit score", "fcra", "nmls", "financing", "adverse action",
1140
+ // robotics-pack
1141
+ "robot", "cobot", "manipulator", "end-effector", "amr", "agv", "autonomous mobile",
1142
+ "surgical robot", "ros 2", "ros2", "drone", "uav",
1143
+ // em-fintech-pack
1144
+ "india", "nigeria", "brazil", "indonesia", "philippines", "mexico", "kenya",
1145
+ "m-pesa", "mpesa", "upi", "pix", "gcash", "ovo", "dana", "rbi", "cbn", "bsp",
1146
+ "ojk", "mas", "bcb", "condusef", "cross-border", "remittance", "local rails",
1147
+ // climate-pack
1148
+ "carbon", "emission", "ghg", "mrv", "scope 1", "scope 2", "scope 3", "verra",
1149
+ "gold standard", "puro", "sbti", "cdp", "csrd", "cbam", "ghgrp", "offset",
1150
+ "credit retir", "removal", "biogenic",
1151
+ "dna synthesis", "gene synthesis", "oligonucleotide", "protein design",
1152
+ "esm", "alphafold", "rfdiffusion", "pathogen", "select agent", "gain-of-function",
1153
+ "dual-use", "bsl-3", "bsl-4", "biocontainment", "bwc", "p3co", "igsc", "cloud lab",
1154
+ // drug-discovery-pack
1155
+ "drug discovery", "binding affinity", "admet", "toxicity prediction",
1156
+ "generative chem", "generative protein", "antibody design", "mrna design",
1157
+ "virtual screening", "docking", "fep", "chembl", "bindingdb", "pdbbind",
1158
+ "glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
1159
+ "lab automation", "robotic biology", "liquid handler", "hamilton", "tecan",
1160
+ "beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila",
1161
+ ];
1162
+ for (const term of packTerms) {
1163
+ if (text.includes(term))
1164
+ kws.add(term);
1165
+ }
1046
1166
  return Array.from(kws).sort();
1047
1167
  }
1048
1168
  function safeGlob(dir, pattern, kind = "file") {
package/dist/main.js CHANGED
@@ -16,7 +16,6 @@ import { pickArchetype, suggestCompliance } from "./archetypes.js";
16
16
  import { install, findInstalledVersions } from "./installer.js";
17
17
  import { enableGreatCto } from "./settings.js";
18
18
  import { bootstrap } from "./bootstrap.js";
19
- import { resolveTelemetryConsent, sendInstallPing, sendUsagePing } from "./telemetry.js";
20
19
  import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
21
20
  import { readFileSync } from "node:fs";
22
21
  import { dirname, join } from "node:path";
@@ -44,9 +43,9 @@ function parseArgs(argv) {
44
43
  force: false,
45
44
  archetype: null,
46
45
  version: null,
47
- noTelemetry: false,
48
46
  useLlm: false,
49
47
  noLlm: false,
48
+ positional: [],
50
49
  };
51
50
  const rest = [];
52
51
  for (let i = 0; i < argv.length; i++) {
@@ -69,8 +68,6 @@ function parseArgs(argv) {
69
68
  args.boardPort = parseInt(argv[++i] ?? "3141", 10);
70
69
  else if (a === "--no-open")
71
70
  args.boardNoOpen = true;
72
- else if (a === "--no-telemetry")
73
- args.noTelemetry = true;
74
71
  else if (a === "--use-llm")
75
72
  args.useLlm = true;
76
73
  else if (a === "--no-llm")
@@ -118,6 +115,7 @@ function parseArgs(argv) {
118
115
  rest.push(a);
119
116
  }
120
117
  args.dir = resolve(args.dir);
118
+ args.positional = rest;
121
119
  return args;
122
120
  }
123
121
  /**
@@ -406,8 +404,6 @@ ${bold("Options:")}
406
404
  even when heuristic confidence is high
407
405
  --no-llm Skip LLM suggestion (run heuristic only)
408
406
  Or set ${cyan("GREATCTO_NO_LLM=1")}
409
- --no-telemetry Skip anonymous install ping
410
- Or set ${cyan("GREATCTO_NO_TELEMETRY=1")}
411
407
  -h, --help Show this help
412
408
  -v, --version Show great-cto CLI version
413
409
 
@@ -742,17 +738,6 @@ async function runInit(args) {
742
738
  if (!bs.created) {
743
739
  log(` ${dim("PROJECT.md already exists at")} ${bs.projectMdPath} ${dim("— kept as-is")}`);
744
740
  }
745
- // ── telemetry (opt-in, fire-and-forget) ─────────────────
746
- try {
747
- const consent = resolveTelemetryConsent(args.noTelemetry);
748
- // Don't await — finish CLI banner first; ping flies in background
749
- void sendInstallPing({
750
- cliVersion: getCliVersion(),
751
- archetype: archetype,
752
- consent,
753
- });
754
- }
755
- catch { /* never block install on telemetry */ }
756
741
  // ── done ─────────────────────────────────────────────────
757
742
  log("");
758
743
  log(green(bold("✓ great_cto is ready.")));
@@ -768,19 +753,6 @@ async function runInit(args) {
768
753
  log("");
769
754
  return 0;
770
755
  }
771
- /**
772
- * Exit with telemetry ping for a subcommand. Fire-and-forget — we wait up
773
- * to 200ms for the ping to land before exiting so it doesn't get killed by
774
- * process termination. Honours all telemetry opt-out signals.
775
- */
776
- async function exitWithTelemetry(subcommand, code) {
777
- try {
778
- const promise = sendUsagePing({ cliVersion: getCliVersion(), subcommand, exitCode: code });
779
- await Promise.race([promise, new Promise(r => setTimeout(r, 200))]);
780
- }
781
- catch { /* never block exit */ }
782
- process.exit(code);
783
- }
784
756
  async function main() {
785
757
  const rawArgv = process.argv.slice(2);
786
758
  const args = parseArgs(rawArgv);
@@ -832,7 +804,7 @@ async function main() {
832
804
  try {
833
805
  const { runCi, parseCiArgs } = await import("./ci.js");
834
806
  const code = await runCi(parseCiArgs(rawArgv));
835
- await exitWithTelemetry("ci", code);
807
+ process.exit(code);
836
808
  }
837
809
  catch (e) {
838
810
  error(e.message);
@@ -846,7 +818,7 @@ async function main() {
846
818
  const portArg = rawArgv.indexOf("--port");
847
819
  const port = portArg >= 0 ? parseInt(rawArgv[portArg + 1] ?? "8765", 10) : 8765;
848
820
  const code = await runMcp({ mode: sse ? "sse" : "stdio", port, version: getCliVersion() });
849
- await exitWithTelemetry("mcp", code);
821
+ process.exit(code);
850
822
  }
851
823
  catch (e) {
852
824
  error(e.message);
@@ -868,7 +840,7 @@ async function main() {
868
840
  dryRun: rawArgv.includes("--dry-run"),
869
841
  cwd: args.dir,
870
842
  });
871
- await exitWithTelemetry("adapt", code);
843
+ process.exit(code);
872
844
  }
873
845
  catch (e) {
874
846
  error(e.message);
@@ -883,7 +855,7 @@ async function main() {
883
855
  noLog: rawArgv.includes("--no-log"),
884
856
  insecure: rawArgv.includes("--insecure"),
885
857
  });
886
- await exitWithTelemetry("serve", code);
858
+ process.exit(code);
887
859
  }
888
860
  catch (e) {
889
861
  error(e.message);
@@ -899,7 +871,7 @@ async function main() {
899
871
  process.exit(2);
900
872
  }
901
873
  const code = await runWebhookCli(parsed);
902
- await exitWithTelemetry("webhook", code);
874
+ process.exit(code);
903
875
  }
904
876
  catch (e) {
905
877
  error(e.message);
@@ -930,7 +902,7 @@ async function main() {
930
902
  process.exit(2);
931
903
  }
932
904
  const code = await runReport(parsed);
933
- await exitWithTelemetry("report", code);
905
+ process.exit(code);
934
906
  }
935
907
  catch (e) {
936
908
  error(e.message);
package/dist/packs.js ADDED
@@ -0,0 +1,132 @@
1
+ // Domain pack detection — overlay packs that ride on top of base archetypes.
2
+ // Wave 1-3 (2026-05): voice, clinical, hr-ai, api-platform, lending, clinical-trials,
3
+ // robotics, em-fintech, climate, drug-discovery.
4
+ //
5
+ // A pack is a regulatory/domain overlay that triggers one or more specialist
6
+ // reviewers (agents/{name}-reviewer.md) when its signals appear in stack,
7
+ // dependencies, or README keywords. Detection is deliberately broad — the
8
+ // reviewer agents themselves do the final scope decision via their Step 0 grep.
9
+ // Reviewer registry — keep in sync with agents/*-reviewer.md and
10
+ // skills/great_cto/packs/*-pack.md.
11
+ const PACK_REVIEWERS = {
12
+ "voice-pack": ["voice-ai-reviewer"],
13
+ "clinical-pack": ["ai-clinical-reviewer", "fda-reviewer"],
14
+ "hr-ai-pack": ["hr-ai-reviewer"],
15
+ "api-platform-pack": ["api-platform-reviewer"],
16
+ "lending-pack": ["lending-credit-reviewer"],
17
+ "clinical-trials-pack": ["clinical-trials-reviewer", "bio-data-reviewer"],
18
+ "robotics-pack": ["robotics-safety-reviewer"],
19
+ "em-fintech-pack": ["emerging-markets-fintech-reviewer"],
20
+ "climate-pack": ["climate-mrv-reviewer", "biosecurity-reviewer"],
21
+ "drug-discovery-pack": ["drug-discovery-ml-reviewer", "glp-glab-reviewer", "lab-automation-reviewer"],
22
+ };
23
+ const PACK_GATES = {
24
+ "voice-pack": ["gate:voice-compliance"],
25
+ "clinical-pack": ["gate:samd-class", "gate:clinical-validation", "gate:ide-approval"],
26
+ "hr-ai-pack": ["gate:aedt-audit"],
27
+ "api-platform-pack": ["gate:api-contract"],
28
+ "lending-pack": ["gate:fair-lending"],
29
+ "clinical-trials-pack": ["gate:irb-ready", "gate:part11-validation", "gate:deidentification"],
30
+ "robotics-pack": ["gate:hara-signoff", "gate:functional-safety-test"],
31
+ "em-fintech-pack": ["gate:license-strategy"],
32
+ "climate-pack": ["gate:mrv-methodology", "gate:durc-signoff", "gate:open-weights-release"],
33
+ "drug-discovery-pack": ["gate:model-card-signoff", "gate:csv-validation", "gate:iq-oq-pq"],
34
+ };
35
+ // Trigger signals — stack tokens OR README keywords.
36
+ const SIGNALS = {
37
+ "voice-pack": {
38
+ stack: ["twilio", "vonage", "livekit", "deepgram", "elevenlabs", "whisper", "hume"],
39
+ keywords: ["voice", "telephony", "ivr", "call-center", "tts", "stt", "speech-to-text", "text-to-speech", "phone", "outbound call", "inbound call", "voice agent"],
40
+ },
41
+ "clinical-pack": {
42
+ stack: ["fhir", "hl7"],
43
+ keywords: ["clinical", "patient", "ehr", "emr", "phi", "hipaa", "diagnos", "triage", "radiolog", "patholog", "samd", "scribe", "telehealth-ai", "medical record", "cds", "clinical decision support"],
44
+ },
45
+ "hr-ai-pack": {
46
+ stack: ["greenhouse", "lever", "ashby", "workday"],
47
+ // "candidate" alone is too generic (SaMD/predicate-candidate clash); use compound terms.
48
+ keywords: ["recruit", "hiring", "candidate screening", "candidate evaluation", "candidate ranking", "resume", "ats", "talent acquisition", "performance review", "workforce scheduling", "employee evaluation", "aedt"],
49
+ },
50
+ "api-platform-pack": {
51
+ stack: ["openapi", "graphql", "grpc", "fastify", "trpc"],
52
+ keywords: ["public api", "partner api", "developer portal", "api key", "webhook", "sdk", "rest api", "graphql api", "openapi"],
53
+ },
54
+ "lending-pack": {
55
+ stack: ["plaid"],
56
+ // "ecoa" removed: collides with clinical "eCOA" (Electronic Clinical Outcome Assessment).
57
+ // Lending-specific signals (loan, fcra, nmls, underwrit, fico, ...) are unambiguous.
58
+ keywords: ["loan", "lending", "credit decision", "underwrit", "bnpl", "buy now pay later", "buy-now-pay-later", "payroll advance", "ewa", "line of credit", "fico", "credit score", "fcra", "nmls", "financing", "adverse action"],
59
+ },
60
+ "clinical-trials-pack": {
61
+ stack: ["fhir", "hl7", "dicom", "redcap"],
62
+ // "ecoa" removed: ambiguous with lending ECOA (Reg B). Other clinical-trial
63
+ // signals (ctms, edc, epro, econsent, cdisc, irb, ...) are unambiguous.
64
+ keywords: ["clinical trial", "ctms", "edc", "epro", "econsent", "esource", "randomization", "rtsm", "irt", "decentralized trial", "ind submission", "21 cfr 11", "cdisc", "sdtm", "adam", "irb"],
65
+ },
66
+ "robotics-pack": {
67
+ stack: ["ros", "ros2", "moveit", "gazebo", "px4"],
68
+ keywords: ["robot", "cobot", "manipulator", "end-effector", "amr", "agv", "autonomous mobile", "surgical robot", "ros 2", "drone", "uav"],
69
+ },
70
+ "em-fintech-pack": {
71
+ stack: [],
72
+ keywords: ["india", "nigeria", "brazil", "indonesia", "philippines", "mexico", "kenya", "m-pesa", "mpesa", "upi", "pix", "gcash", "ovo", "dana", "rbi", "cbn", "bsp", "ojk", "mas", "bcb", "condusef", "cross-border", "remittance", "local rails"],
73
+ },
74
+ "climate-pack": {
75
+ stack: [],
76
+ keywords: ["carbon", "emission", "ghg", "mrv", "scope 1", "scope 2", "scope 3", "verra", "gold standard", "puro", "sbti", "cdp", "csrd", "cbam", "ghgrp", "offset", "credit retir", "removal", "biogenic",
77
+ // biosec triggers
78
+ "dna synthesis", "gene synthesis", "oligonucleotide", "protein design", "esm", "alphafold", "rfdiffusion", "pathogen", "select agent", "gain-of-function", "dual-use", "bsl-3", "bsl-4", "biocontainment", "bwc", "p3co", "igsc", "cloud lab"],
79
+ },
80
+ "drug-discovery-pack": {
81
+ stack: ["rdkit"],
82
+ keywords: ["drug discovery", "binding affinity", "admet", "toxicity prediction", "generative chem", "generative protein", "antibody design", "mrna design", "virtual screening", "docking", "fep", "alphafold", "rfdiffusion", "chembl", "bindingdb", "pdbbind",
83
+ // GLP / lab-automation triggers
84
+ "glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
85
+ "lab automation", "cloud lab", "robotic biology", "liquid handler", "hamilton", "tecan", "beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila"],
86
+ },
87
+ };
88
+ /** Return packs whose signals match the detection result. Deterministic + sorted. */
89
+ export function suggestPacks(d) {
90
+ const matches = [];
91
+ const stackLower = new Set(d.stack.map((s) => s.toLowerCase()));
92
+ const kwLower = d.readmeKeywords.map((k) => k.toLowerCase());
93
+ for (const pack of Object.keys(SIGNALS)) {
94
+ const { stack, keywords } = SIGNALS[pack];
95
+ const matchedStack = stack.filter((s) => stackLower.has(s.toLowerCase()));
96
+ // Exact-match on README-mined raw tokens. detect.ts emits the same vocabulary,
97
+ // so kwLower.includes(kw) avoids false-positive substring fuzz
98
+ // (e.g. "lending" matching pack trigger "ind").
99
+ const matchedKeywords = keywords.filter((kw) => kwLower.includes(kw));
100
+ const signals = [...matchedStack, ...matchedKeywords];
101
+ if (signals.length > 0) {
102
+ matches.push({
103
+ pack,
104
+ reviewers: PACK_REVIEWERS[pack],
105
+ signals: signals.slice(0, 8), // cap for log readability
106
+ humanGates: PACK_GATES[pack],
107
+ });
108
+ }
109
+ }
110
+ matches.sort((a, b) => a.pack.localeCompare(b.pack));
111
+ return matches;
112
+ }
113
+ /** Convenience — flatten matched packs to reviewer agent names (unique, sorted). */
114
+ export function suggestPackReviewers(d) {
115
+ const all = new Set();
116
+ for (const m of suggestPacks(d))
117
+ for (const r of m.reviewers)
118
+ all.add(r);
119
+ return Array.from(all).sort();
120
+ }
121
+ /** Convenience — flatten matched packs to human-gate ids (unique, sorted). */
122
+ export function suggestPackGates(d) {
123
+ const all = new Set();
124
+ for (const m of suggestPacks(d))
125
+ for (const g of m.humanGates)
126
+ all.add(g);
127
+ return Array.from(all).sort();
128
+ }
129
+ /** Registry of all known packs — useful for /doctor / introspection. */
130
+ export function listPacks() {
131
+ return Object.keys(SIGNALS).sort();
132
+ }
package/dist/telemetry.js CHANGED
@@ -1,34 +1,68 @@
1
- // Anonymous opt-in telemetry.
1
+ // Anonymous opt-IN telemetry — default OFF.
2
2
  //
3
- // What we send: random install_id (UUID), version, archetype, node version,
4
- // platform, and timestamp. Nothing personal — no email, paths, code, or repo
5
- // names. The install_id is generated once and stored in ~/.great_cto/config.json.
3
+ // See docs/PRIVACY.md for the full policy. Short version:
4
+ // - Default: disabled (opt-in)
5
+ // - Honors DO_NOT_TRACK=1 (industry standard, https://consoledonottrack.com)
6
+ // - Skipped automatically in CI environments
7
+ // - No paths, no code, no PII — just {ts, version, command, archetype, node, os, exit, duration_ms, anon_id}
8
+ // - anon_id is sha256(user@hostname) truncated to 8 hex chars; not reversible
6
9
  //
7
- // What we DON'T send: project paths, code, file names, environment variables,
8
- // shell history, IP-derived geolocation (CF only logs country at the edge).
10
+ // Opt-in (any one):
11
+ // GREAT_CTO_TELEMETRY=on (env var)
12
+ // ~/.great_cto/telemetry.json: { "enabled": true }
13
+ // npx great-cto telemetry on
9
14
  //
10
- // Opt-out:
11
- // - GREATCTO_NO_TELEMETRY=1 env var (highest priority)
12
- // - --no-telemetry CLI flag
13
- // - User declines the first-run prompt
14
- // - Manually edit ~/.great_cto/config.json: { "telemetry": false }
15
+ // Opt-out (overrides everything):
16
+ // DO_NOT_TRACK=1 (highest priority)
17
+ // GREAT_CTO_TELEMETRY=off
18
+ // GREAT_CTO_DISABLE_TELEMETRY=1 (legacy alias from v2.x)
19
+ // GREATCTO_NO_TELEMETRY=1 (legacy alias from v2.x)
20
+ // ~/.great_cto/telemetry.json: { "enabled": false }
15
21
  //
16
- // Endpoint: https://greatcto.systems/api/install (Cloudflare Worker → D1)
17
- // Source: workers/telemetry/index.js
22
+ // Endpoint: https://telemetry.greatcto.systems/v1/event (Cloudflare Worker → D1)
23
+ // Worker: workers/telemetry/index.ts
24
+ // Schema v1: see docs/PRIVACY.md "What we collect"
18
25
  import * as fs from "node:fs";
19
26
  import * as path from "node:path";
20
27
  import * as os from "node:os";
21
28
  import * as crypto from "node:crypto";
22
- import { dim, log } from "./ui.js";
23
- const TELEMETRY_ENDPOINT = "https://greatcto.systems/api/install";
24
- const TELEMETRY_TIMEOUT_MS = 1500;
29
+ const TELEMETRY_ENDPOINT = process.env.GREAT_CTO_TELEMETRY_ENDPOINT
30
+ || "https://great-cto-telemetry.alexander-velikiy.workers.dev/v1/event";
31
+ // Note: workers.dev URL is the temporary default until telemetry.greatcto.systems
32
+ // custom domain is bound. Override anytime with GREAT_CTO_TELEMETRY_ENDPOINT.
33
+ const TELEMETRY_TIMEOUT_MS = 1000;
34
+ // Allowlist — anything else is dropped client-side and server-side.
35
+ const ALLOWED_COMMANDS = new Set([
36
+ "init", "scan", "ci", "list-rules", "board", "register",
37
+ "adapt", "mcp", "report", "serve", "webhook",
38
+ "version", "help", "telemetry",
39
+ ]);
40
+ // Allowlist for archetype field. Match the 25 documented + "none" + "unknown".
41
+ const ALLOWED_ARCHETYPES = new Set([
42
+ "none", "unknown", "greenfield",
43
+ "enterprise-saas", "agent-product", "ai-system", "mlops",
44
+ "cli-tool", "cli", "library", "sdk", "devtools",
45
+ "fintech", "regulated", "compliance",
46
+ "iot-embedded", "web3", "marketplace", "cms", "edtech",
47
+ "gov-public", "insurance", "data-platform", "streaming",
48
+ "mobile-app", "infra", "web-service", "agent",
49
+ ]);
25
50
  function configPath() {
51
+ return path.join(os.homedir(), ".great_cto", "telemetry.json");
52
+ }
53
+ function legacyConfigPath() {
26
54
  return path.join(os.homedir(), ".great_cto", "config.json");
27
55
  }
28
56
  function readConfig() {
57
+ // Try new file first.
58
+ try {
59
+ return JSON.parse(fs.readFileSync(configPath(), "utf8"));
60
+ }
61
+ catch { /* fall through */ }
62
+ // Fall back to legacy config.json (read-only — never write to it).
29
63
  try {
30
- const raw = fs.readFileSync(configPath(), "utf8");
31
- return JSON.parse(raw);
64
+ const legacy = JSON.parse(fs.readFileSync(legacyConfigPath(), "utf8"));
65
+ return { enabled: legacy.telemetry, install_id: legacy.install_id };
32
66
  }
33
67
  catch {
34
68
  return {};
@@ -39,121 +73,153 @@ function writeConfig(cfg) {
39
73
  fs.mkdirSync(path.dirname(file), { recursive: true });
40
74
  fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
41
75
  }
42
- function ensureInstallId(cfg) {
43
- if (cfg.install_id && /^[0-9a-f-]{36}$/i.test(cfg.install_id))
44
- return cfg.install_id;
45
- const id = crypto.randomUUID();
46
- cfg.install_id = id;
47
- writeConfig(cfg);
48
- return id;
76
+ /** Detect CI / automation environments — never send from these. */
77
+ function isCI() {
78
+ const flags = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI", "BUILDKITE",
79
+ "JENKINS_URL", "TF_BUILD", "DRONE", "TRAVIS", "APPVEYOR",
80
+ "BITBUCKET_BUILD_NUMBER", "TEAMCITY_VERSION", "CODEBUILD_BUILD_ID"];
81
+ return flags.some(f => process.env[f] != null && process.env[f] !== "");
49
82
  }
50
- /**
51
- * Decide whether telemetry is enabled for this run. May write to config.json
52
- * the first time the user is prompted. Pure-read in subsequent runs.
53
- *
54
- * Resolution order:
55
- * 1. GREATCTO_NO_TELEMETRY=1 env var false
56
- * 2. --no-telemetry flag (passed in `cliFlag`) → false
57
- * 3. Stored config.telemetry that value
58
- * 4. Default to enabled (true) if non-interactive (e.g. CI), else show notice
59
- */
60
- export function resolveTelemetryConsent(cliFlag) {
83
+ /** Compute anon_id deterministically per machine, never reversible. */
84
+ export function computeAnonId() {
85
+ const seed = `great_cto/${os.userInfo().username || "?"}/${os.hostname() || "?"}`;
86
+ return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 8);
87
+ }
88
+ /** Resolve telemetry-enabled state. Pure function, no side effects. */
89
+ export function isTelemetryEnabled(cliFlag = false) {
90
+ // Opt-out wins, in priority order:
91
+ if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true")
92
+ return false;
93
+ if (process.env.GREAT_CTO_TELEMETRY === "off")
94
+ return false;
95
+ if (process.env.GREAT_CTO_DISABLE_TELEMETRY === "1")
96
+ return false;
61
97
  if (process.env.GREATCTO_NO_TELEMETRY === "1")
62
98
  return false;
63
99
  if (cliFlag)
64
100
  return false;
101
+ if (isCI())
102
+ return false;
103
+ // Opt-in checks:
104
+ if (process.env.GREAT_CTO_TELEMETRY === "on")
105
+ return true;
65
106
  const cfg = readConfig();
66
- if (typeof cfg.telemetry === "boolean")
67
- return cfg.telemetry;
68
- // First-run notice. We default to enabled (privacy-respecting opt-out) but
69
- // show a clear notice with how to disable. Kept short; full details in README.
70
- log("");
71
- log(dim("─ Anonymous telemetry ────────────────────────────────"));
72
- log(dim(" great_cto sends one anonymous ping per install:"));
73
- log(dim(" install_id, version, archetype, Node version, OS."));
74
- log(dim(" No paths, no code, no PII. Disable any time:"));
75
- log(dim(" great-cto --no-telemetry · or set GREATCTO_NO_TELEMETRY=1"));
76
- log(dim(" or edit ~/.great_cto/config.json: { \"telemetry\": false }"));
77
- log(dim("──────────────────────────────────────────────────────"));
78
- log("");
79
- cfg.telemetry = true;
80
- cfg.telemetry_asked = true;
81
- ensureInstallId(cfg);
82
- writeConfig(cfg);
83
- return true;
107
+ if (cfg.enabled === true)
108
+ return true;
109
+ if (cfg.telemetry === true)
110
+ return true; // legacy
111
+ // Default: opt-out.
112
+ return false;
84
113
  }
85
- /**
86
- * Best-effort telemetry ping. Non-blocking, fire-and-forget. Never throws.
87
- * Returns a promise that resolves once the request completes or times out.
88
- */
89
- export async function sendInstallPing(opts) {
90
- if (!opts.consent)
91
- return;
92
- const cfg = readConfig();
93
- const install_id = ensureInstallId(cfg);
94
- const evt = {
95
- install_id,
96
- cli_version: opts.cliVersion,
97
- archetype: opts.archetype,
98
- node_version: process.version,
99
- platform: process.platform,
100
- arch: process.arch,
114
+ function sanitize(opts) {
115
+ const command = opts.command.toLowerCase();
116
+ if (!ALLOWED_COMMANDS.has(command))
117
+ return null;
118
+ const archetypeRaw = (opts.archetype || "none").toLowerCase().trim();
119
+ const archetype = ALLOWED_ARCHETYPES.has(archetypeRaw) ? archetypeRaw : "unknown";
120
+ return {
101
121
  ts: new Date().toISOString(),
122
+ version: opts.cliVersion,
123
+ command,
124
+ archetype,
125
+ node: process.version.replace(/^v/, ""),
126
+ os: process.platform,
127
+ exit_code: typeof opts.exitCode === "number" ? opts.exitCode : 0,
128
+ duration_ms: typeof opts.durationMs === "number" ? Math.max(0, Math.round(opts.durationMs)) : 0,
129
+ anon_id: computeAnonId(),
102
130
  };
131
+ }
132
+ /** Fire-and-forget POST. Never blocks. Never throws. Never logs unless DRYRUN. */
133
+ async function send(evt) {
134
+ if (process.env.GREAT_CTO_TELEMETRY_DRYRUN === "1") {
135
+ process.stderr.write(`[telemetry] would-send: ${JSON.stringify(evt)}\n`);
136
+ return;
137
+ }
103
138
  try {
104
139
  const ctrl = new AbortController();
105
140
  const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
106
141
  await fetch(TELEMETRY_ENDPOINT, {
107
142
  method: "POST",
108
- headers: { "Content-Type": "application/json", "User-Agent": `great-cto-cli/${opts.cliVersion}` },
143
+ headers: { "Content-Type": "application/json" },
109
144
  body: JSON.stringify(evt),
110
145
  signal: ctrl.signal,
111
146
  }).catch(() => { });
112
147
  clearTimeout(timer);
113
148
  }
114
- catch {
115
- // never block install on telemetry failure
116
- }
149
+ catch { /* best-effort */ }
117
150
  }
118
- /**
119
- * Subcommand-usage ping. Fire-and-forget. Used to track which v2.4+ commands
120
- * (ci / mcp / adapt / serve / report / webhook) actually get used in the wild.
121
- *
122
- * Sends only:
123
- * - install_id (random UUID, set on first install)
124
- * - cli_version
125
- * - subcommand name
126
- * - exit code (0 / 1 / 2)
127
- *
128
- * No paths, no flags (since flags often contain user input), no archetype.
129
- * Honours the same opt-out signals as install ping.
130
- */
151
+ // --- Public API ------------------------------------------------------------
152
+ /** First-run/install ping. Sent only when enabled. Idempotent across runs. */
153
+ export async function sendInstallPing(opts) {
154
+ if (!opts.consent)
155
+ return;
156
+ if (!isTelemetryEnabled())
157
+ return;
158
+ const evt = sanitize({ cliVersion: opts.cliVersion, command: "init", archetype: opts.archetype });
159
+ if (!evt)
160
+ return;
161
+ await send(evt);
162
+ }
163
+ /** Per-command usage ping. Sent only when enabled. Fire-and-forget. */
131
164
  export async function sendUsagePing(opts) {
132
- if (process.env.GREATCTO_NO_TELEMETRY === "1")
165
+ if (!isTelemetryEnabled())
133
166
  return;
134
- const cfg = readConfig();
135
- if (cfg.telemetry === false)
167
+ const evt = sanitize({
168
+ cliVersion: opts.cliVersion,
169
+ command: opts.subcommand,
170
+ archetype: opts.archetype,
171
+ exitCode: opts.exitCode,
172
+ durationMs: opts.durationMs,
173
+ });
174
+ if (!evt)
136
175
  return;
137
- if (!cfg.install_id)
138
- return; // never ping without an established install_id
139
- try {
140
- const ctrl = new AbortController();
141
- const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
142
- await fetch(`${TELEMETRY_ENDPOINT.replace("/install", "/usage")}`, {
143
- method: "POST",
144
- headers: { "Content-Type": "application/json", "User-Agent": `great-cto-cli/${opts.cliVersion}` },
145
- body: JSON.stringify({
146
- install_id: cfg.install_id,
147
- cli_version: opts.cliVersion,
148
- subcommand: opts.subcommand,
149
- exit_code: opts.exitCode,
150
- ts: new Date().toISOString(),
151
- }),
152
- signal: ctrl.signal,
153
- }).catch(() => { });
154
- clearTimeout(timer);
155
- }
156
- catch {
157
- // best-effort
176
+ await send(evt);
177
+ }
178
+ /**
179
+ * Legacy shim — preserved for backwards compatibility with callers in main.ts
180
+ * that pass `--no-telemetry`. With opt-IN default, consent resolution is
181
+ * trivial: enabled iff isTelemetryEnabled() returns true.
182
+ */
183
+ export function resolveTelemetryConsent(cliFlag) {
184
+ return isTelemetryEnabled(cliFlag);
185
+ }
186
+ // --- `npx great-cto telemetry <on|off|status|whoami>` subcommand -----------
187
+ export function telemetrySubcommand(arg) {
188
+ const action = (arg || "status").toLowerCase();
189
+ switch (action) {
190
+ case "on": {
191
+ const cfg = readConfig();
192
+ cfg.enabled = true;
193
+ writeConfig(cfg);
194
+ return { exitCode: 0, output: `✓ telemetry enabled (config: ${configPath()})\n` +
195
+ ` Anonymous events go to ${TELEMETRY_ENDPOINT}\n` +
196
+ ` See docs/PRIVACY.md for the full data schema.\n` };
197
+ }
198
+ case "off": {
199
+ const cfg = readConfig();
200
+ cfg.enabled = false;
201
+ writeConfig(cfg);
202
+ return { exitCode: 0, output: `✓ telemetry disabled (config: ${configPath()})\n` };
203
+ }
204
+ case "status": {
205
+ const enabled = isTelemetryEnabled();
206
+ const reason = enabled
207
+ ? "enabled (sending events to " + TELEMETRY_ENDPOINT + ")"
208
+ : isCI()
209
+ ? "disabled (CI environment detected)"
210
+ : process.env.DO_NOT_TRACK === "1"
211
+ ? "disabled (DO_NOT_TRACK=1)"
212
+ : "disabled (default; run 'great-cto telemetry on' to enable)";
213
+ return { exitCode: 0, output: `telemetry: ${reason}\n` +
214
+ `anon_id : ${computeAnonId()}\n` +
215
+ `endpoint : ${TELEMETRY_ENDPOINT}\n` +
216
+ `config : ${configPath()}\n` };
217
+ }
218
+ case "whoami": {
219
+ return { exitCode: 0, output: computeAnonId() + "\n" };
220
+ }
221
+ default: {
222
+ return { exitCode: 2, output: `usage: great-cto telemetry <on|off|status|whoami>\n` };
223
+ }
158
224
  }
159
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
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",