great-cto 2.8.6 → 2.9.2

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.
package/dist/adapt.js CHANGED
@@ -77,6 +77,44 @@ Configure Claude Code to launch:
77
77
  npx great-cto mcp
78
78
  \`\`\`
79
79
 
80
+ ## Subagent routing (do not default to general-purpose)
81
+
82
+ When dispatching the **Agent** tool, pick the right \`subagent_type\` based on
83
+ what's being changed. \`general-purpose\` is a fallback, **not** the default —
84
+ using it for pattern-matched work silently skips specialist review.
85
+
86
+ | Trigger (file pattern OR topic in prompt) | Use \`subagent_type:\` |
87
+ |---|---|
88
+ | \`migrations/\`, \`schema.sql\`, ORM migration files | \`db-migration-reviewer\` |
89
+ | \`auth/\`, OAuth/SAML/JWT, login flow, password reset | \`security-officer\` |
90
+ | Payment endpoints, \`stripe.\`, webhooks, refund flow | \`pci-reviewer\` |
91
+ | Prompts in \`prompts/\`, RAG, tool definitions, LLM-facing strings | \`ai-security-reviewer\` |
92
+ | Eval suites, golden citation tests, prompt regression | \`ai-eval-engineer\` |
93
+ | Play Store, App Store, iOS/Android release | \`mobile-store-reviewer\` |
94
+ | API contract: OpenAPI, GraphQL schema, webhook signatures | \`api-platform-reviewer\` |
95
+ | Voice/IVR/telephony, Twilio, recording-consent, TCPA | \`voice-ai-reviewer\` |
96
+ | Clinical / SaMD / FDA / FHIR / PHI | \`ai-clinical-reviewer\`, \`fda-reviewer\` |
97
+ | Lending, ECOA, FCRA, NMLS, adverse action | \`lending-credit-reviewer\` |
98
+ | HR-AI, hiring, AEDT, resume screening | \`hr-ai-reviewer\` |
99
+ | Infra-as-code: Terraform / Helm / CDK / Pulumi | \`infra-reviewer\` |
100
+ | Performance regression, hot path, p99 budgets | \`performance-engineer\` |
101
+ | Browser extension manifest, MV3 permissions | \`web-store-reviewer\` |
102
+ | New feature implementation (TDD) | \`senior-dev\` |
103
+ | Architecture decisions, ADRs, scaling questions | \`architect\` |
104
+ | Planning a feature into tasks (Beads), dependency graph | \`pm\` |
105
+ | QA report after impl, coverage + acceptance | \`qa-engineer\` |
106
+ | Deploy / canary / rollback / SLO | \`devops\` |
107
+ | Production incident triage, P0 postmortem | \`l3-support\` |
108
+ | Pattern extraction from session, retro to \`lessons.md\` | \`continuous-learner\` |
109
+
110
+ Rule of thumb: if a file pattern or topic matches above, use that specialist
111
+ **before** falling back to \`general-purpose\`. Specialist agents catch
112
+ domain-specific bugs (race conditions in migrations, prompt-injection vectors,
113
+ TCPA violations) that a generalist won't flag.
114
+
115
+ When in doubt, run two agents in parallel: the specialist + general-purpose,
116
+ and reconcile their outputs.
117
+
80
118
  ## Style + conventions
81
119
 
82
120
  - Tests **before** implementation (RED → GREEN → REFACTOR). Coverage 80%+ default.
@@ -11,6 +11,7 @@
11
11
  // - slack: posts as Slack incoming-webhook JSON ({text, blocks?})
12
12
  // - discord: Discord webhook JSON ({content, embeds?})
13
13
  // - pagerduty: Events API v2 ({routing_key, event_action, payload})
14
+ // - resend: HTML email via Resend API ({from, to, subject, html})
14
15
  // - generic: arbitrary JSON POST
15
16
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
16
17
  import { homedir } from "node:os";
@@ -39,6 +40,62 @@ function formatDiscord(ev) {
39
40
  embeds: ev.body ? [{ description: ev.body, color }] : undefined,
40
41
  };
41
42
  }
43
+ function emojiForLevel(level) {
44
+ return level === "critical" ? "🚨"
45
+ : level === "error" ? "❌"
46
+ : level === "warning" ? "⏸️"
47
+ : "ℹ️";
48
+ }
49
+ function escapeHtml(s) {
50
+ return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
51
+ }
52
+ /**
53
+ * Build the Resend API payload — POSTed to https://api.resend.com/emails
54
+ * with Authorization: Bearer <api_key>.
55
+ *
56
+ * Body fields from meta:
57
+ * - project, link (CTA URL), action (CTA label)
58
+ * - kv: Record<string,string> for the metric table
59
+ * - severity (optional override for color)
60
+ */
61
+ function formatResend(ev, hook) {
62
+ const accent = ev.level === "critical" ? "#dc2626"
63
+ : ev.level === "error" ? "#ea580c"
64
+ : ev.level === "warning" ? "#d97706"
65
+ : "#00d97e";
66
+ const emoji = emojiForLevel(ev.level);
67
+ const meta = (ev.meta ?? {});
68
+ const project = typeof meta.project === "string" ? meta.project : "great_cto";
69
+ const link = typeof meta.link === "string" ? meta.link : "";
70
+ const action = typeof meta.action === "string" ? meta.action : "View in board";
71
+ const kv = (meta.kv ?? {});
72
+ const tableRows = Object.entries(kv)
73
+ .map(([k, v]) => `<tr><td style="padding:6px 12px;color:#6b7280;font-size:12px;font-family:ui-monospace,monospace;text-transform:uppercase;letter-spacing:.05em">${escapeHtml(k)}</td><td style="padding:6px 12px;color:#111827;font-size:14px;font-weight:500">${escapeHtml(v)}</td></tr>`)
74
+ .join("");
75
+ const html = `<!doctype html><html><body style="margin:0;background:#f9fafb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111827">
76
+ <div style="max-width:560px;margin:32px auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden">
77
+ <div style="padding:20px 24px;border-bottom:1px solid #e5e7eb;background:#0a0e0c;color:#ffffff">
78
+ <div style="font-size:11px;font-family:ui-monospace,monospace;letter-spacing:.1em;color:#9ca3af">${escapeHtml(project.toUpperCase())} · GREATCTO</div>
79
+ <div style="font-size:20px;font-weight:600;margin-top:6px;color:${accent}">${emoji} ${escapeHtml(ev.title)}</div>
80
+ </div>
81
+ ${ev.body ? `<div style="padding:20px 24px;font-size:14px;line-height:1.55;color:#374151">${escapeHtml(ev.body).replace(/\n/g, "<br>")}</div>` : ""}
82
+ ${tableRows ? `<table style="width:100%;border-top:1px solid #e5e7eb;border-collapse:collapse">${tableRows}</table>` : ""}
83
+ ${link ? `<div style="padding:24px;text-align:center;border-top:1px solid #e5e7eb">
84
+ <a href="${escapeHtml(link)}" style="display:inline-block;background:${accent};color:#ffffff;text-decoration:none;padding:10px 22px;border-radius:8px;font-weight:600;font-size:14px">${escapeHtml(action)}</a>
85
+ </div>` : ""}
86
+ <div style="padding:14px 24px;background:#f9fafb;font-size:11px;color:#9ca3af;font-family:ui-monospace,monospace">
87
+ Sent by great_cto · ${escapeHtml(ev.name)} · ${new Date().toISOString()}<br>
88
+ Unsubscribe: edit ~/.great_cto/webhooks.json or the Notifications tab in the board
89
+ </div>
90
+ </div></body></html>`;
91
+ const to = (hook.to ?? "").split(",").map(s => s.trim()).filter(Boolean);
92
+ return {
93
+ from: hook.from ?? "GreatCTO <notifications@greatcto.systems>",
94
+ to,
95
+ subject: `${emoji} ${ev.title}`,
96
+ html,
97
+ };
98
+ }
42
99
  function formatPagerDuty(ev, routingKey) {
43
100
  // PagerDuty Events API v2
44
101
  const severity = ev.level === "critical" ? "critical"
@@ -65,6 +122,7 @@ function buildPayload(hook, ev) {
65
122
  const key = hook.headers?.routing_key ?? "";
66
123
  return formatPagerDuty(ev, key);
67
124
  }
125
+ case "resend": return formatResend(ev, hook);
68
126
  case "generic":
69
127
  default: return { event: ev.name, ...ev };
70
128
  }
@@ -79,7 +137,18 @@ async function deliver(hook, ev, attempt = 0) {
79
137
  };
80
138
  // Don't leak routing_key as HTTP header — PagerDuty wants it in body
81
139
  delete headers.routing_key;
82
- const res = await fetch(hook.url, { method: "POST", headers, body });
140
+ // Resend: Bearer auth + URL override (config can leave url blank).
141
+ let url = hook.url;
142
+ if (hook.format === "resend") {
143
+ if (!hook.apiKey)
144
+ throw new Error("resend: apiKey is required");
145
+ if (!hook.to)
146
+ throw new Error("resend: 'to' email is required");
147
+ headers["Authorization"] = `Bearer ${hook.apiKey}`;
148
+ if (!url)
149
+ url = "https://api.resend.com/emails";
150
+ }
151
+ const res = await fetch(url, { method: "POST", headers, body });
83
152
  if (!res.ok) {
84
153
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
85
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.8.6",
3
+ "version": "2.9.2",
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",
@@ -86,4 +86,4 @@
86
86
  "engines": {
87
87
  "node": ">=18.17.0"
88
88
  }
89
- }
89
+ }
package/dist/telemetry.js DELETED
@@ -1,225 +0,0 @@
1
- // Anonymous opt-IN telemetry — default OFF.
2
- //
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
9
- //
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
14
- //
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 }
21
- //
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"
25
- import * as fs from "node:fs";
26
- import * as path from "node:path";
27
- import * as os from "node:os";
28
- import * as crypto from "node:crypto";
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
- ]);
50
- function configPath() {
51
- return path.join(os.homedir(), ".great_cto", "telemetry.json");
52
- }
53
- function legacyConfigPath() {
54
- return path.join(os.homedir(), ".great_cto", "config.json");
55
- }
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).
63
- try {
64
- const legacy = JSON.parse(fs.readFileSync(legacyConfigPath(), "utf8"));
65
- return { enabled: legacy.telemetry, install_id: legacy.install_id };
66
- }
67
- catch {
68
- return {};
69
- }
70
- }
71
- function writeConfig(cfg) {
72
- const file = configPath();
73
- fs.mkdirSync(path.dirname(file), { recursive: true });
74
- fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
75
- }
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] !== "");
82
- }
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;
97
- if (process.env.GREATCTO_NO_TELEMETRY === "1")
98
- return false;
99
- if (cliFlag)
100
- return false;
101
- if (isCI())
102
- return false;
103
- // Opt-in checks:
104
- if (process.env.GREAT_CTO_TELEMETRY === "on")
105
- return true;
106
- const cfg = readConfig();
107
- if (cfg.enabled === true)
108
- return true;
109
- if (cfg.telemetry === true)
110
- return true; // legacy
111
- // Default: opt-out.
112
- return false;
113
- }
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 {
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(),
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
- }
138
- try {
139
- const ctrl = new AbortController();
140
- const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
141
- await fetch(TELEMETRY_ENDPOINT, {
142
- method: "POST",
143
- headers: { "Content-Type": "application/json" },
144
- body: JSON.stringify(evt),
145
- signal: ctrl.signal,
146
- }).catch(() => { });
147
- clearTimeout(timer);
148
- }
149
- catch { /* best-effort */ }
150
- }
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. */
164
- export async function sendUsagePing(opts) {
165
- if (!isTelemetryEnabled())
166
- return;
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)
175
- return;
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
- }
224
- }
225
- }