great-cto 2.3.3 → 2.4.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.
package/dist/adapt.js ADDED
@@ -0,0 +1,342 @@
1
+ // great-cto adapt — platform config generator.
2
+ //
3
+ // Writes platform-native config files derived from a shared core. Lets a
4
+ // single great_cto installation work transparently with Claude Code, OpenAI
5
+ // Codex CLI, Cursor, Aider, and Continue. AGENTS.md is the de-facto cross-
6
+ // platform standard (used verbatim by Codex; consumed as fallback by most
7
+ // others) so it forms the shared core.
8
+ //
9
+ // Usage:
10
+ // great-cto adapt --platform claude CLAUDE.md
11
+ // great-cto adapt --platform codex AGENTS.md
12
+ // great-cto adapt --platform cursor .cursorrules + .cursor/rules/*.mdc
13
+ // great-cto adapt --platform aider .aider.conf.yml + CONVENTIONS.md
14
+ // great-cto adapt --platform continue .continue/rules.md
15
+ // great-cto adapt --platform all all of the above
16
+ // great-cto adapt --dry-run show what would be written
17
+ //
18
+ // Each adapter writes ONLY platform-native files. All share the AGENTS.md
19
+ // core text via getAgentsCore(). To customize per-project, edit
20
+ // .great_cto/PROJECT.md (archetype, owners, compliance) — adapt re-derives.
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ function readProjectMeta(cwd) {
24
+ const projectMd = join(cwd, ".great_cto", "PROJECT.md");
25
+ if (!existsSync(projectMd)) {
26
+ return { archetype: "unknown", compliance: [], owners: "", hasGreatCto: false };
27
+ }
28
+ const text = readFileSync(projectMd, "utf8");
29
+ const archetype = (text.match(/^primary:\s*(\S+)/m)?.[1] ?? "unknown").trim();
30
+ const complianceLine = text.match(/^compliance:\s*(.+)$/m)?.[1] ?? "";
31
+ const compliance = complianceLine
32
+ .split(/[,\s]+/)
33
+ .map(s => s.trim())
34
+ .filter(Boolean);
35
+ const owners = (text.match(/^owners?:\s*(.+)$/m)?.[1] ?? "").trim();
36
+ return { archetype, compliance, owners, hasGreatCto: true };
37
+ }
38
+ /**
39
+ * Common AGENTS.md content used across platforms.
40
+ * Codex CLI reads this verbatim. Cursor / Aider / Claude Code embed it.
41
+ */
42
+ function getAgentsCore(meta) {
43
+ const compliance = meta.compliance.length > 0
44
+ ? meta.compliance.map(c => `- ${c}`).join("\n")
45
+ : "_(none auto-detected — set in .great_cto/PROJECT.md)_";
46
+ return `# AGENTS.md
47
+
48
+ > This file is the cross-tool agent contract. It is consumed by OpenAI Codex
49
+ > CLI, Claude Code, Cursor, Aider, Continue, and any other AGENTS.md-aware
50
+ > tooling. Generated by \`great-cto adapt\` — re-run after editing
51
+ > \`.great_cto/PROJECT.md\`.
52
+
53
+ ## Project context
54
+
55
+ - **Archetype:** ${meta.archetype}
56
+ - **Compliance gates:**
57
+ ${compliance.split("\n").map(l => " " + l).join("\n")}
58
+ - **Owners:** ${meta.owners || "_(unset)_"}
59
+
60
+ ## How agents should work in this repo
61
+
62
+ 1. **Before touching code** — read \`.great_cto/PROJECT.md\` (archetype +
63
+ constraints) and \`.great_cto/lessons.md\` if present (past mistakes).
64
+ 2. **Before proposing a fix** — query \`~/.great_cto/decisions.md\` for ADRs
65
+ on related topics. Solved problems should stay solved.
66
+ 3. **Before merging** — run \`npx great-cto ci\` locally. Same gate as CI.
67
+ 4. **On failure / incident** — append to \`.great_cto/lessons.md\`. After 3
68
+ occurrences across projects, \`/crystallize\` promotes to global pattern.
69
+
70
+ ## Required gates (do not bypass)
71
+
72
+ | Gate | When | Owner |
73
+ |---|---|---|
74
+ | security-officer | Any change touching auth, payments, PII | @security |
75
+ | qa-engineer | Any feature merge | @qa |
76
+ | performance-engineer | Any change to hot path | @perf |
77
+ | db-migration-reviewer | Any \`migrations/\` change | @data |
78
+
79
+ Override with \`/waiver\` and a written reason. Auto-expires in 14 days.
80
+
81
+ ## Available tools (via MCP)
82
+
83
+ When this repo is opened in any MCP-capable tool, \`great-cto mcp\` exposes:
84
+
85
+ - \`scan\` — OWASP LLM Top 10 + 24 rules · returns findings
86
+ - \`list_rules\` — full rule catalogue
87
+ - \`detect_archetype\` — archetype + compliance for any path
88
+ - \`estimate_cost\` — LLM vs human time estimate for a task
89
+ - \`query_decisions\` — search ADR log
90
+
91
+ Configure your client to launch:
92
+ \`\`\`bash
93
+ npx great-cto mcp
94
+ \`\`\`
95
+
96
+ ## Style + conventions
97
+
98
+ - Tests **before** implementation (RED → GREEN → REFACTOR). Coverage 80%+ default.
99
+ - Conventional commits: \`feat\` / \`fix\` / \`docs\` / \`refactor\` / \`test\` / \`chore\`.
100
+ - One concern per PR. Refactors and behaviour changes are separate PRs.
101
+ - No secrets in code. \`great-cto scan\` blocks ~13 patterns by default.
102
+
103
+ ## Out of scope
104
+
105
+ Do not invent business decisions. If the spec is ambiguous, ask. Do not skip
106
+ gates "because it's just a small change" — that's the path to incidents.
107
+
108
+ ---
109
+
110
+ _Generated by \`great-cto@${getCliVersion()}\` adapt at ${new Date().toISOString().slice(0, 10)}_
111
+ `;
112
+ }
113
+ function getCliVersion() {
114
+ try {
115
+ const here = dirname(new URL(import.meta.url).pathname);
116
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
117
+ return pkg.version ?? "unknown";
118
+ }
119
+ catch {
120
+ return "unknown";
121
+ }
122
+ }
123
+ function writeFile(path, content, dryRun) {
124
+ if (dryRun) {
125
+ console.log(`would write: ${path} (${content.length} bytes)`);
126
+ return false;
127
+ }
128
+ mkdirSync(dirname(path), { recursive: true });
129
+ const existed = existsSync(path);
130
+ writeFileSync(path, content);
131
+ console.log(` ${existed ? "✎ updated" : "✓ wrote"} ${path}`);
132
+ return true;
133
+ }
134
+ // ── Per-platform adapters ──────────────────────────────────────────────────
135
+ function adaptClaude(cwd, meta, dryRun) {
136
+ // Claude Code reads CLAUDE.md (or AGENTS.md as fallback). We write both
137
+ // for maximum compat — projects with CLAUDE.md already get a top-level
138
+ // reference, and AGENTS.md unlocks any AGENTS.md-aware tool simultaneously.
139
+ const out = [];
140
+ const agentsBody = getAgentsCore(meta);
141
+ const claudeBody = `# CLAUDE.md
142
+
143
+ > Project guidance for Claude Code. The full agent contract lives in
144
+ > \`AGENTS.md\` — please read that first. This file adds Claude-Code-specific
145
+ > overrides only.
146
+
147
+ ## Cross-tool contract
148
+
149
+ See [AGENTS.md](./AGENTS.md) for the full project context, gates, and
150
+ conventions. Generated by \`great-cto adapt\`.
151
+
152
+ ## Claude Code specifics
153
+
154
+ - The great_cto plugin orchestrates 34 specialist agents — pipeline runs
155
+ through \`/start\`, \`/inbox\`, \`/save\`, etc.
156
+ - Memory is layered: \`.great_cto/PROJECT.md\` (L1) → \`lessons.md\` (L3) →
157
+ \`~/.great_cto/decisions.md\` (L4 cross-project ADR log).
158
+ - Run \`great-cto board\` (\`http://localhost:3141\`) for the kanban + metrics
159
+ + memory + agents view.
160
+
161
+ ## Quick links
162
+
163
+ - ${"`"}/start "..."${"`"} — kick off a feature pipeline
164
+ - ${"`"}/inbox${"`"} — see what needs your decision
165
+ - ${"`"}/agent-review${"`"} — performance scorecard for agents
166
+
167
+ `;
168
+ if (writeFile(join(cwd, "AGENTS.md"), agentsBody, dryRun))
169
+ out.push("AGENTS.md");
170
+ if (writeFile(join(cwd, "CLAUDE.md"), claudeBody, dryRun))
171
+ out.push("CLAUDE.md");
172
+ return out;
173
+ }
174
+ function adaptCodex(cwd, meta, dryRun) {
175
+ // OpenAI Codex CLI reads AGENTS.md. We write only that — Codex doesn't
176
+ // currently consume CLAUDE.md / .cursorrules.
177
+ const out = [];
178
+ if (writeFile(join(cwd, "AGENTS.md"), getAgentsCore(meta), dryRun))
179
+ out.push("AGENTS.md");
180
+ return out;
181
+ }
182
+ function adaptCursor(cwd, meta, dryRun) {
183
+ // Cursor reads .cursorrules (legacy single file) and .cursor/rules/*.mdc
184
+ // (modern modular). Write both for compat.
185
+ const out = [];
186
+ const rulesContent = `# Cursor rules — auto-generated by great-cto adapt
187
+ # See AGENTS.md for the full agent contract; this file is the Cursor-native
188
+ # subset focusing on inline-completion and chat behaviour.
189
+
190
+ archetype: ${meta.archetype}
191
+ compliance:
192
+ ${meta.compliance.map(c => ` - ${c}`).join("\n") || " - none"}
193
+
194
+ ## Behaviour
195
+
196
+ Before suggesting code changes:
197
+ - Read \`AGENTS.md\` for repo-wide conventions
198
+ - Read \`.great_cto/PROJECT.md\` for archetype constraints
199
+ - Check \`~/.great_cto/decisions.md\` for prior ADRs on the topic
200
+
201
+ ## Gates that block merges (do not bypass)
202
+
203
+ - security-officer: auth/payments/PII changes
204
+ - qa-engineer: feature merges
205
+ - db-migration-reviewer: any migrations/ change
206
+
207
+ ## Style
208
+
209
+ - TDD: tests RED → implementation GREEN → refactor
210
+ - Conventional commits
211
+ - One concern per PR
212
+ `;
213
+ const mdcContent = `---
214
+ description: great_cto archetype + compliance contract
215
+ globs: ["**/*"]
216
+ alwaysApply: true
217
+ ---
218
+
219
+ This repo is a **${meta.archetype}** project. Compliance gates:
220
+ ${meta.compliance.map(c => `- ${c}`).join("\n") || "- none"}
221
+
222
+ Read \`AGENTS.md\` and \`.great_cto/PROJECT.md\` before proposing changes.
223
+ Run \`npx great-cto ci\` before pushing.
224
+ `;
225
+ if (writeFile(join(cwd, ".cursorrules"), rulesContent, dryRun))
226
+ out.push(".cursorrules");
227
+ if (writeFile(join(cwd, ".cursor", "rules", "great-cto.mdc"), mdcContent, dryRun))
228
+ out.push(".cursor/rules/great-cto.mdc");
229
+ if (writeFile(join(cwd, "AGENTS.md"), getAgentsCore(meta), dryRun))
230
+ out.push("AGENTS.md");
231
+ return out;
232
+ }
233
+ function adaptAider(cwd, meta, dryRun) {
234
+ // Aider reads .aider.conf.yml for settings and CONVENTIONS.md (or files
235
+ // listed in read: arrays) for context.
236
+ const out = [];
237
+ const conf = `# .aider.conf.yml — auto-generated by great-cto adapt
238
+ # Aider config. Run aider in this dir and these settings apply.
239
+
240
+ # Always include AGENTS.md and PROJECT.md as context
241
+ read:
242
+ - AGENTS.md
243
+ - .great_cto/PROJECT.md
244
+
245
+ # Auto-test after edits
246
+ auto-test: true
247
+ test-cmd: "npx great-cto ci --severity high"
248
+
249
+ # Pretty output
250
+ pretty: true
251
+
252
+ # Conventional commits
253
+ commit-prompt: "Use conventional commit format: feat/fix/docs/refactor/test/chore"
254
+
255
+ # Archetype: ${meta.archetype}
256
+ # Compliance: ${meta.compliance.join(", ") || "none"}
257
+ `;
258
+ const conventions = `# CONVENTIONS.md
259
+
260
+ > This is the Aider-native conventions file. The cross-tool agent contract
261
+ > lives in \`AGENTS.md\` — please read that for full context.
262
+
263
+ ## Quick rules
264
+
265
+ - **Archetype:** ${meta.archetype}
266
+ - **Compliance gates:** ${meta.compliance.join(", ") || "_(none)_"}
267
+
268
+ ### Process
269
+ 1. Tests first (TDD)
270
+ 2. One concern per PR
271
+ 3. Conventional commits
272
+ 4. Run \`npx great-cto ci\` before push
273
+
274
+ ### Style
275
+ - No secrets in code (\`great-cto scan\` enforces this)
276
+ - Read \`.great_cto/PROJECT.md\` before suggesting architectural changes
277
+ - Read \`~/.great_cto/decisions.md\` for prior ADRs
278
+ `;
279
+ if (writeFile(join(cwd, ".aider.conf.yml"), conf, dryRun))
280
+ out.push(".aider.conf.yml");
281
+ if (writeFile(join(cwd, "CONVENTIONS.md"), conventions, dryRun))
282
+ out.push("CONVENTIONS.md");
283
+ if (writeFile(join(cwd, "AGENTS.md"), getAgentsCore(meta), dryRun))
284
+ out.push("AGENTS.md");
285
+ return out;
286
+ }
287
+ function adaptContinue(cwd, meta, dryRun) {
288
+ const out = [];
289
+ const rulesContent = `# Continue rules — generated by great-cto adapt
290
+
291
+ ## Project context
292
+
293
+ Archetype: **${meta.archetype}**.
294
+ Compliance: ${meta.compliance.join(", ") || "none"}.
295
+ Read \`AGENTS.md\` for the full contract.
296
+
297
+ ## Behaviour
298
+
299
+ - Run \`npx great-cto ci\` before pushing
300
+ - Don't bypass gates (security / qa / db-migration)
301
+ - Append lessons to \`.great_cto/lessons.md\` after incidents
302
+ `;
303
+ if (writeFile(join(cwd, ".continue", "rules.md"), rulesContent, dryRun))
304
+ out.push(".continue/rules.md");
305
+ if (writeFile(join(cwd, "AGENTS.md"), getAgentsCore(meta), dryRun))
306
+ out.push("AGENTS.md");
307
+ return out;
308
+ }
309
+ // ── Main entry ─────────────────────────────────────────────────────────────
310
+ export async function runAdapt(args) {
311
+ const meta = readProjectMeta(args.cwd);
312
+ if (!meta.hasGreatCto) {
313
+ console.error("FAIL: no .great_cto/PROJECT.md found.");
314
+ console.error("Run `npx great-cto init` first to bootstrap the project.");
315
+ return 1;
316
+ }
317
+ const platforms = args.platform === "all"
318
+ ? ["claude", "codex", "cursor", "aider", "continue"]
319
+ : [args.platform];
320
+ console.log(`great-cto adapt → ${platforms.join(", ")}${args.dryRun ? " (dry-run)" : ""}`);
321
+ console.log(` archetype: ${meta.archetype} compliance: ${meta.compliance.join(", ") || "none"}`);
322
+ console.log("");
323
+ const written = [];
324
+ for (const p of platforms) {
325
+ console.log(`▸ ${p}`);
326
+ const adapter = {
327
+ claude: adaptClaude,
328
+ codex: adaptCodex,
329
+ cursor: adaptCursor,
330
+ aider: adaptAider,
331
+ continue: adaptContinue,
332
+ }[p];
333
+ const out = adapter(args.cwd, meta, args.dryRun);
334
+ written.push(...out);
335
+ }
336
+ if (!args.dryRun) {
337
+ console.log("");
338
+ console.log(`✓ generated ${written.length} file(s) for ${platforms.length} platform(s).`);
339
+ console.log(` Re-run after editing .great_cto/PROJECT.md to refresh.`);
340
+ }
341
+ return 0;
342
+ }
package/dist/ci.js ADDED
@@ -0,0 +1,258 @@
1
+ // great-cto ci — single-command CI gate.
2
+ //
3
+ // Runs scan + budget-check + archetype-validate with output formats matching
4
+ // CI conventions. Designed to be the only great_cto invocation in a CI step.
5
+ //
6
+ // Outputs:
7
+ // - human-readable to stderr (always)
8
+ // - GitHub Actions annotations (auto when $GITHUB_ACTIONS is set, or --annotations)
9
+ // - SARIF 2.1.0 JSON (--sarif <path>) — uploadable to GitHub Security tab
10
+ // - JUnit XML (--junit <path>) — for test reporters / pipeline UIs
11
+ //
12
+ // Exit codes:
13
+ // 0 = clean, all gates pass
14
+ // 1 = findings at/above --fail-on threshold (CI should fail)
15
+ // 2 = scan/setup error (not a finding — infrastructure problem)
16
+ //
17
+ // Example workflow:
18
+ // - run: npx great-cto@latest ci ./
19
+ // env:
20
+ // GREAT_CTO_NO_TELEMETRY: "1"
21
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { resolve } from "node:path";
23
+ const SEVERITY_ORDER = {
24
+ info: 0,
25
+ low: 1,
26
+ medium: 2,
27
+ high: 3,
28
+ critical: 4,
29
+ };
30
+ function severityAtLeast(sev, threshold) {
31
+ return (SEVERITY_ORDER[sev] ?? 0) >= (SEVERITY_ORDER[threshold] ?? 0);
32
+ }
33
+ /**
34
+ * Emit GitHub Actions annotation lines. These appear inline on PR diffs.
35
+ * Format: ::error file=path,line=N,col=M,title=T::message
36
+ * ::warning file=...
37
+ * ::notice file=...
38
+ */
39
+ function emitGitHubAnnotation(finding) {
40
+ const sev = finding.rule.severity;
41
+ const level = sev === "critical" || sev === "high" ? "error"
42
+ : sev === "medium" ? "warning" : "notice";
43
+ const file = finding.location.file;
44
+ const line = finding.location.line;
45
+ const title = `${finding.rule.id}: ${finding.rule.title}`;
46
+ const message = finding.rule.owasp
47
+ ? `${finding.rule.title} (${finding.rule.owasp})`
48
+ : finding.rule.title;
49
+ // Escape GHA-special characters
50
+ const escape = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
51
+ console.log(`::${level} file=${escape(file)},line=${line},title=${escape(title)}::${escape(message)}`);
52
+ }
53
+ /**
54
+ * Emit JUnit XML report. One <testcase> per scanned file, fails recorded as
55
+ * <failure> elements. Format compatible with most CI test reporters.
56
+ */
57
+ function buildJunitXml(report) {
58
+ const findingsByFile = new Map();
59
+ for (const f of report.findings) {
60
+ const arr = findingsByFile.get(f.location.file) || [];
61
+ arr.push(f);
62
+ findingsByFile.set(f.location.file, arr);
63
+ }
64
+ const totalTests = Math.max(report.filesScanned, findingsByFile.size);
65
+ const failures = report.findings.length;
66
+ const escape = (s) => String(s)
67
+ .replace(/&/g, "&amp;")
68
+ .replace(/</g, "&lt;")
69
+ .replace(/>/g, "&gt;")
70
+ .replace(/"/g, "&quot;")
71
+ .replace(/'/g, "&apos;");
72
+ const cases = Array.from(findingsByFile.entries())
73
+ .map(([file, findings]) => {
74
+ const failureBody = findings
75
+ .map(f => ` <failure type="${escape(f.rule.severity)}" message="${escape(f.rule.id + ': ' + f.rule.title)}">
76
+ ${escape(f.location.snippet || '')}
77
+ ${escape(f.rule.owasp || '')}
78
+ </failure>`)
79
+ .join("\n");
80
+ return ` <testcase classname="agentshield" name="${escape(file)}" time="0">
81
+ ${failureBody}
82
+ </testcase>`;
83
+ })
84
+ .join("\n");
85
+ return `<?xml version="1.0" encoding="UTF-8"?>
86
+ <testsuites>
87
+ <testsuite name="great-cto ci" tests="${totalTests}" failures="${failures}" errors="0" time="${(report.durationMs / 1000).toFixed(3)}">
88
+ ${cases}
89
+ </testsuite>
90
+ </testsuites>
91
+ `;
92
+ }
93
+ /**
94
+ * Quick archetype-detection sanity check. Fails CI if the archetype changed
95
+ * from what's pinned in .great_cto/PROJECT.md (signals undeclared
96
+ * architectural drift).
97
+ */
98
+ async function archetypeCheck(cwd, quiet) {
99
+ const projectMd = resolve(cwd, ".great_cto", "PROJECT.md");
100
+ if (!existsSync(projectMd)) {
101
+ if (!quiet)
102
+ console.error(" ⊘ archetype check skipped (no .great_cto/PROJECT.md)");
103
+ return { ok: true, msg: "skipped" };
104
+ }
105
+ const declared = (readFileSync(projectMd, "utf8").match(/^primary:\s*(\S+)/m)?.[1] ?? "").trim();
106
+ if (!declared) {
107
+ return { ok: true, msg: "no archetype declared in PROJECT.md" };
108
+ }
109
+ try {
110
+ const { detect } = await import("./detect.js");
111
+ const { pickArchetype } = await import("./archetypes.js");
112
+ const detected = await detect(cwd);
113
+ const result = pickArchetype(detected);
114
+ if (result.primary !== declared) {
115
+ return {
116
+ ok: false,
117
+ msg: `archetype drift: declared=${declared}, detected=${result.primary} (${result.confidence})`,
118
+ };
119
+ }
120
+ return { ok: true, msg: `archetype confirmed: ${declared}` };
121
+ }
122
+ catch (e) {
123
+ return { ok: true, msg: `archetype check failed (non-fatal): ${e.message}` };
124
+ }
125
+ }
126
+ /**
127
+ * Quick budget sanity check. Reads monthly-budget from PROJECT.md and warns
128
+ * if recent burn (last 30 days) exceeds it. Non-fatal — never blocks CI.
129
+ * Pure observability — for fatal budget enforcement use cost-guard hook.
130
+ */
131
+ function budgetCheck(cwd, quiet) {
132
+ const projectMd = resolve(cwd, ".great_cto", "PROJECT.md");
133
+ if (!existsSync(projectMd))
134
+ return { ok: true, msg: "no PROJECT.md" };
135
+ const text = readFileSync(projectMd, "utf8");
136
+ const budget = text.match(/monthly[-_]budget:\s*\$?(\d[\d,]+)/i)?.[1]?.replace(/,/g, "");
137
+ if (!budget)
138
+ return { ok: true, msg: "no budget set" };
139
+ // Very simple: just confirm budget is well-formed. Real burn calc lives in board.
140
+ if (!quiet)
141
+ console.error(` ✓ monthly-budget: $${budget}`);
142
+ return { ok: true, msg: `budget configured: $${budget}` };
143
+ }
144
+ export async function runCi(args) {
145
+ const startTs = Date.now();
146
+ const inGitHubActions = process.env.GITHUB_ACTIONS === "true";
147
+ const wantAnnotations = args.annotations || inGitHubActions;
148
+ if (!args.quiet) {
149
+ console.error(`\ngreat-cto ci — gate threshold: ${args.failOn}, scan: ${args.severity}+`);
150
+ console.error(` path: ${resolve(args.path)}`);
151
+ if (inGitHubActions)
152
+ console.error(` env: GitHub Actions detected — emitting annotations`);
153
+ console.error("");
154
+ }
155
+ // 1. Scan
156
+ let scan;
157
+ let toSarif;
158
+ try {
159
+ ({ scan } = await import("./agentshield/scanner.js"));
160
+ ({ toSarif } = await import("./agentshield/sarif.js"));
161
+ }
162
+ catch (e) {
163
+ console.error(`ci: failed to load scanner: ${e.message}`);
164
+ return 2;
165
+ }
166
+ const report = scan(resolve(args.path), {
167
+ minSeverity: args.severity,
168
+ });
169
+ // 2. Archetype check
170
+ let archResult = { ok: true, msg: "skipped" };
171
+ if (!args.noArchetype) {
172
+ archResult = await archetypeCheck(args.path, args.quiet);
173
+ }
174
+ // 3. Budget check (warn-only)
175
+ let budgetResult = { ok: true, msg: "skipped" };
176
+ if (!args.noBudget) {
177
+ budgetResult = budgetCheck(args.path, args.quiet);
178
+ }
179
+ // 4. Emit annotations
180
+ if (wantAnnotations) {
181
+ for (const f of report.findings) {
182
+ if (severityAtLeast(f.rule.severity, args.failOn)) {
183
+ emitGitHubAnnotation(f);
184
+ }
185
+ }
186
+ }
187
+ // 5. Emit SARIF
188
+ if (args.sarifPath) {
189
+ writeFileSync(args.sarifPath, JSON.stringify(toSarif(report), null, 2));
190
+ if (!args.quiet)
191
+ console.error(` ✓ SARIF → ${args.sarifPath}`);
192
+ }
193
+ // 6. Emit JUnit XML
194
+ if (args.junitPath) {
195
+ writeFileSync(args.junitPath, buildJunitXml(report));
196
+ if (!args.quiet)
197
+ console.error(` ✓ JUnit XML → ${args.junitPath}`);
198
+ }
199
+ // 7. Summary
200
+ const blockingFindings = report.findings.filter((f) => severityAtLeast(f.rule.severity, args.failOn));
201
+ const passed = blockingFindings.length === 0 && archResult.ok;
202
+ if (!args.quiet) {
203
+ const dur = ((Date.now() - startTs) / 1000).toFixed(1);
204
+ console.error("");
205
+ console.error(` scan: ${report.findings.length} finding(s) (${blockingFindings.length} at/above ${args.failOn})`);
206
+ console.error(` archetype: ${archResult.ok ? "✓" : "✗"} ${archResult.msg}`);
207
+ console.error(` budget: ${budgetResult.msg}`);
208
+ console.error(` duration: ${dur}s`);
209
+ console.error("");
210
+ if (passed) {
211
+ console.error("\x1b[32m✓ great-cto ci: passed\x1b[0m");
212
+ }
213
+ else {
214
+ console.error("\x1b[31m✗ great-cto ci: failed\x1b[0m");
215
+ if (blockingFindings.length) {
216
+ console.error(` ${blockingFindings.length} finding(s) at/above ${args.failOn}:`);
217
+ for (const f of blockingFindings.slice(0, 10)) {
218
+ console.error(` [${f.rule.severity}] ${f.rule.id} — ${f.location.file}:${f.location.line}`);
219
+ }
220
+ if (blockingFindings.length > 10) {
221
+ console.error(` ... +${blockingFindings.length - 10} more`);
222
+ }
223
+ }
224
+ if (!archResult.ok)
225
+ console.error(` ${archResult.msg}`);
226
+ }
227
+ }
228
+ return passed ? 0 : 1;
229
+ }
230
+ /**
231
+ * Parse `great-cto ci` flags from raw argv.
232
+ */
233
+ export function parseCiArgs(rawArgv) {
234
+ const flag = (n) => rawArgv.includes(`--${n}`);
235
+ const value = (n, def) => {
236
+ const i = rawArgv.indexOf(`--${n}`);
237
+ return i >= 0 && i < rawArgv.length - 1 ? rawArgv[i + 1] : def;
238
+ };
239
+ const ciIdx = rawArgv.indexOf("ci");
240
+ let path = ".";
241
+ for (let i = ciIdx + 1; i < rawArgv.length; i++) {
242
+ if (rawArgv[i] && !rawArgv[i].startsWith("--")) {
243
+ path = rawArgv[i];
244
+ break;
245
+ }
246
+ }
247
+ return {
248
+ path,
249
+ severity: value("severity", "high"),
250
+ failOn: value("fail-on", "critical"),
251
+ sarifPath: value("sarif") ?? null,
252
+ junitPath: value("junit") ?? null,
253
+ annotations: flag("annotations"),
254
+ noBudget: flag("no-budget"),
255
+ noArchetype: flag("no-archetype"),
256
+ quiet: flag("quiet"),
257
+ };
258
+ }
package/dist/main.js CHANGED
@@ -83,6 +83,14 @@ function parseArgs(argv) {
83
83
  args.command = "scan";
84
84
  else if (a === "list-rules")
85
85
  args.command = "list-rules";
86
+ else if (a === "ci")
87
+ args.command = "ci";
88
+ else if (a === "mcp")
89
+ args.command = "mcp";
90
+ else if (a === "adapt")
91
+ args.command = "adapt";
92
+ else if (a === "serve")
93
+ args.command = "serve";
86
94
  else if (a.startsWith("--dir="))
87
95
  args.dir = a.slice("--dir=".length);
88
96
  else if (a === "--dir")
@@ -316,6 +324,10 @@ ${bold("Usage:")}
316
324
  npx great-cto register [--dir PATH]
317
325
  npx great-cto scan [path] [--severity LVL] [--scanner NAME] [--sarif FILE]
318
326
  npx great-cto list-rules
327
+ npx great-cto ci [path] [--fail-on LVL] [--sarif F] [--junit F]
328
+ npx great-cto mcp [--sse --port N]
329
+ npx great-cto adapt --platform [claude|codex|cursor|aider|continue|all]
330
+ npx great-cto serve [--port 3142]
319
331
  npx great-cto help
320
332
  npx great-cto version
321
333
 
@@ -338,6 +350,32 @@ ${bold("Scan (AI-security):")}
338
350
  great-cto list-rules Print rule catalog
339
351
  ${dim("(exits 1 if findings ≥ severity threshold; CI-friendly)")}
340
352
 
353
+ ${bold("CI gate:")}
354
+ great-cto ci Single-command CI gate (scan + archetype check)
355
+ great-cto ci --fail-on critical Exit 1 only on critical findings (default)
356
+ great-cto ci --sarif out.sarif Emit SARIF (uploadable to GitHub Security)
357
+ great-cto ci --junit out.xml Emit JUnit XML for test reporters
358
+ ${dim("(auto-detects \$GITHUB_ACTIONS → emits ::error:: annotations)")}
359
+
360
+ ${bold("MCP server (cross-platform):")}
361
+ great-cto mcp Stdio MCP server — works in Claude Desktop /
362
+ Cursor / Continue / any MCP host
363
+ great-cto mcp --sse --port 8765 SSE mode for remote / multi-client (TODO v2.5)
364
+ ${dim("Tools exposed: scan, list_rules, detect_archetype, estimate_cost, query_decisions")}
365
+
366
+ ${bold("Platform adapter (multi-tool support):")}
367
+ great-cto adapt --platform claude Generate AGENTS.md + CLAUDE.md
368
+ great-cto adapt --platform codex Generate AGENTS.md (OpenAI Codex CLI)
369
+ great-cto adapt --platform cursor Generate .cursorrules + .cursor/rules/*.mdc
370
+ great-cto adapt --platform aider Generate .aider.conf.yml + CONVENTIONS.md
371
+ great-cto adapt --platform continue Generate .continue/rules.md
372
+ great-cto adapt --platform all All of the above
373
+ ${dim("Idempotent — re-run after editing .great_cto/PROJECT.md")}
374
+
375
+ ${bold("Webhook server (preview):")}
376
+ great-cto serve --port 3142 Webhook receiver (logs to ~/.great_cto/webhook-events.log)
377
+ ${dim("Endpoints: POST /webhook/github, POST /webhook/generic, GET /events, GET /healthz")}
378
+
341
379
  ${bold("Options:")}
342
380
  -y, --yes Skip confirmation prompts (non-interactive)
343
381
  --dry-run Show what would be done without doing it
@@ -732,6 +770,67 @@ async function main() {
732
770
  process.exit(1);
733
771
  }
734
772
  }
773
+ if (args.command === "ci") {
774
+ try {
775
+ const { runCi, parseCiArgs } = await import("./ci.js");
776
+ const code = await runCi(parseCiArgs(rawArgv));
777
+ process.exit(code);
778
+ }
779
+ catch (e) {
780
+ error(e.message);
781
+ process.exit(2);
782
+ }
783
+ }
784
+ if (args.command === "mcp") {
785
+ try {
786
+ const { runMcp } = await import("./mcp.js");
787
+ const sse = rawArgv.includes("--sse");
788
+ const portArg = rawArgv.indexOf("--port");
789
+ const port = portArg >= 0 ? parseInt(rawArgv[portArg + 1] ?? "8765", 10) : 8765;
790
+ const code = await runMcp({ mode: sse ? "sse" : "stdio", port, version: getCliVersion() });
791
+ process.exit(code);
792
+ }
793
+ catch (e) {
794
+ error(e.message);
795
+ process.exit(2);
796
+ }
797
+ }
798
+ if (args.command === "adapt") {
799
+ try {
800
+ const { runAdapt } = await import("./adapt.js");
801
+ const platArg = rawArgv.indexOf("--platform");
802
+ const platform = (platArg >= 0 ? rawArgv[platArg + 1] : "all");
803
+ const valid = ["claude", "codex", "cursor", "aider", "continue", "all"];
804
+ if (!valid.includes(platform)) {
805
+ error(`adapt: --platform must be one of ${valid.join(", ")} (got: ${platform})`);
806
+ process.exit(2);
807
+ }
808
+ const code = await runAdapt({
809
+ platform,
810
+ dryRun: rawArgv.includes("--dry-run"),
811
+ cwd: args.dir,
812
+ });
813
+ process.exit(code);
814
+ }
815
+ catch (e) {
816
+ error(e.message);
817
+ process.exit(2);
818
+ }
819
+ }
820
+ if (args.command === "serve") {
821
+ try {
822
+ const { runServe } = await import("./serve.js");
823
+ const code = await runServe({
824
+ port: args.boardPort === 3141 ? 3142 : args.boardPort,
825
+ noLog: rawArgv.includes("--no-log"),
826
+ });
827
+ process.exit(code);
828
+ }
829
+ catch (e) {
830
+ error(e.message);
831
+ process.exit(2);
832
+ }
833
+ }
735
834
  if (args.command === "version") {
736
835
  // Version resolved in index.mjs or from package.json at runtime
737
836
  try {
package/dist/mcp.js ADDED
@@ -0,0 +1,287 @@
1
+ // great-cto mcp — Model Context Protocol server.
2
+ //
3
+ // Exposes great_cto's core capabilities as MCP tools so any MCP-compatible
4
+ // host (Claude Desktop, Cursor, Continue, Codex CLI via MCP, custom agents)
5
+ // can call them. This is the single biggest cross-platform multiplier — one
6
+ // implementation, all clients.
7
+ //
8
+ // Modes:
9
+ // stdio (default) — Claude Desktop / Cursor / Continue spawn this as subprocess
10
+ // sse — long-running HTTP/SSE for remote / multi-client access
11
+ //
12
+ // Tools exposed:
13
+ // scan OWASP LLM Top 10 + 24 rules → findings
14
+ // list_rules full rule catalogue
15
+ // detect_archetype archetype + compliance for a path
16
+ // estimate_cost LLM/human time for a task
17
+ // query_decisions search ~/.great_cto/decisions.md
18
+ //
19
+ // Protocol: minimal MCP 2024-11-05 implementation. We hand-roll because
20
+ // adding @modelcontextprotocol/sdk would balloon install size for what is
21
+ // fundamentally a few JSON messages over stdio.
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join, resolve } from "node:path";
25
+ const PROTOCOL_VERSION = "2024-11-05";
26
+ const SERVER_INFO = {
27
+ name: "great-cto",
28
+ version: "", // populated at startup
29
+ };
30
+ // ── Tool implementations ────────────────────────────────────────────────────
31
+ async function toolScan(args) {
32
+ const { scan } = await import("./agentshield/scanner.js");
33
+ const path = args.path ?? ".";
34
+ const report = scan(resolve(path), {
35
+ minSeverity: (args.severity ?? "info"),
36
+ scanners: args.scanner,
37
+ });
38
+ return {
39
+ files_scanned: report.filesScanned,
40
+ duration_ms: report.durationMs,
41
+ findings: report.findings.map((f) => ({
42
+ rule_id: f.rule.id,
43
+ severity: f.rule.severity,
44
+ title: f.rule.title,
45
+ owasp: f.rule.owasp,
46
+ file: f.location.file,
47
+ line: f.location.line,
48
+ snippet: f.location.snippet,
49
+ })),
50
+ };
51
+ }
52
+ async function toolListRules() {
53
+ const { loadRules } = await import("./agentshield/rules-loader.js");
54
+ const rules = loadRules();
55
+ return {
56
+ count: rules.length,
57
+ rules: rules.map((r) => ({
58
+ id: r.id,
59
+ severity: r.severity,
60
+ scanner: r.scanner,
61
+ title: r.title,
62
+ owasp: r.owasp ?? null,
63
+ })),
64
+ };
65
+ }
66
+ async function toolDetectArchetype(args) {
67
+ const { detect } = await import("./detect.js");
68
+ const { pickArchetype, suggestCompliance } = await import("./archetypes.js");
69
+ const cwd = resolve(args.path ?? ".");
70
+ const detected = await detect(cwd);
71
+ const result = pickArchetype(detected);
72
+ const compliance = suggestCompliance(detected, result.primary);
73
+ return {
74
+ archetype: result.primary,
75
+ confidence: result.confidence,
76
+ rationale: result.rationale,
77
+ alternatives: result.alternatives,
78
+ compliance,
79
+ };
80
+ }
81
+ async function toolEstimateCost(args) {
82
+ // Rough heuristic — for production, agents call /cost feature directly.
83
+ // This is the LLM-host-friendly summary.
84
+ const scale = args.scale ?? "standard";
85
+ const minutesByScale = {
86
+ quick: 15,
87
+ standard: 45,
88
+ deep: 90,
89
+ };
90
+ const llmRatePerHr = 0.02;
91
+ const humanRatePerHr = 150;
92
+ const minutes = minutesByScale[scale] ?? 45;
93
+ const llmUsd = +(minutes / 60 * llmRatePerHr).toFixed(4);
94
+ const humanUsd = +(minutes / 60 * humanRatePerHr).toFixed(2);
95
+ const humanDays = +(minutes / 60 / 6).toFixed(1); // 6 productive hrs / day
96
+ return {
97
+ task: args.task_description ?? "",
98
+ archetype: args.archetype ?? "unknown",
99
+ scale,
100
+ llm_agent: { wall_clock_min: minutes, cost_usd: llmUsd },
101
+ human_team: { working_days: humanDays, cost_usd: humanUsd },
102
+ savings_x: Math.round(humanUsd / Math.max(llmUsd, 0.0001)),
103
+ };
104
+ }
105
+ function toolQueryDecisions(args) {
106
+ const decisionsPath = join(homedir(), ".great_cto", "decisions.md");
107
+ if (!existsSync(decisionsPath)) {
108
+ return { count: 0, results: [], note: "No ~/.great_cto/decisions.md found." };
109
+ }
110
+ const text = readFileSync(decisionsPath, "utf8");
111
+ const entries = text.split(/\n---\n/).filter(s => s.trim());
112
+ const q = (args.query ?? "").toLowerCase();
113
+ const limit = args.limit ?? 10;
114
+ const matches = q
115
+ ? entries.filter(e => e.toLowerCase().includes(q))
116
+ : entries.slice(-limit);
117
+ return {
118
+ count: matches.length,
119
+ total_decisions: entries.length,
120
+ results: matches.slice(0, limit).map(e => {
121
+ const titleMatch = e.match(/^##\s+(.+)$/m);
122
+ return {
123
+ title: titleMatch?.[1] ?? "(untitled)",
124
+ excerpt: e.slice(0, 400),
125
+ };
126
+ }),
127
+ };
128
+ }
129
+ // ── Tool dispatch table ────────────────────────────────────────────────────
130
+ const TOOLS = [
131
+ {
132
+ name: "scan",
133
+ description: "Scan code for AI/LLM-specific security issues (OWASP LLM Top 10, 24 rules). Returns findings with severity, file, line, OWASP mapping.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ path: { type: "string", description: "File or directory to scan (default: cwd)" },
138
+ severity: {
139
+ type: "string",
140
+ enum: ["info", "low", "medium", "high", "critical"],
141
+ description: "Minimum severity to report",
142
+ },
143
+ scanner: {
144
+ type: "array",
145
+ items: {
146
+ type: "string",
147
+ enum: ["prompt-injection", "secrets-in-prompts", "ssrf-in-tools", "rag-poisoning", "cost-runaway"],
148
+ },
149
+ description: "Limit to specific scanner categories",
150
+ },
151
+ },
152
+ },
153
+ handler: toolScan,
154
+ },
155
+ {
156
+ name: "list_rules",
157
+ description: "List all 24 AgentShield security rules with severity and OWASP LLM Top 10 mapping.",
158
+ inputSchema: { type: "object", properties: {} },
159
+ handler: toolListRules,
160
+ },
161
+ {
162
+ name: "detect_archetype",
163
+ description: "Detect the project archetype (one of 25: fintech, healthcare, commerce, agent-product, mlops, edtech, gov-public, insurance, ...) and the compliance gates that apply.",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {
167
+ path: { type: "string", description: "Project root (default: cwd)" },
168
+ },
169
+ },
170
+ handler: toolDetectArchetype,
171
+ },
172
+ {
173
+ name: "estimate_cost",
174
+ description: "Estimate LLM-agent wall-clock time and cost vs human-team equivalent for a task. Returns LLM/human cost and savings ratio (~7500×).",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ task_description: { type: "string" },
179
+ archetype: { type: "string" },
180
+ scale: { type: "string", enum: ["quick", "standard", "deep"] },
181
+ },
182
+ },
183
+ handler: toolEstimateCost,
184
+ },
185
+ {
186
+ name: "query_decisions",
187
+ description: "Search the cross-project ADR log at ~/.great_cto/decisions.md. Returns matching decisions for the given query string.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ query: { type: "string", description: "Search string (case-insensitive). Empty = recent decisions." },
192
+ limit: { type: "number", description: "Max results (default 10)" },
193
+ },
194
+ },
195
+ handler: toolQueryDecisions,
196
+ },
197
+ ];
198
+ // ── JSON-RPC handler ───────────────────────────────────────────────────────
199
+ async function handle(req) {
200
+ const { method, id = null, params } = req;
201
+ // Notifications (no id) get no response
202
+ const isNotification = id === null || id === undefined;
203
+ const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: id, result };
204
+ const fail = (code, message, data) => isNotification ? null : { jsonrpc: "2.0", id: id, error: { code, message, data } };
205
+ try {
206
+ switch (method) {
207
+ case "initialize":
208
+ return reply({
209
+ protocolVersion: PROTOCOL_VERSION,
210
+ capabilities: { tools: {} },
211
+ serverInfo: SERVER_INFO,
212
+ });
213
+ case "initialized":
214
+ case "notifications/initialized":
215
+ return null;
216
+ case "tools/list":
217
+ return reply({
218
+ tools: TOOLS.map(t => ({
219
+ name: t.name,
220
+ description: t.description,
221
+ inputSchema: t.inputSchema,
222
+ })),
223
+ });
224
+ case "tools/call": {
225
+ const name = params?.name;
226
+ const args = params?.arguments ?? {};
227
+ const tool = TOOLS.find(t => t.name === name);
228
+ if (!tool)
229
+ return fail(-32601, `Unknown tool: ${name}`);
230
+ const result = await tool.handler(args);
231
+ return reply({
232
+ content: [
233
+ { type: "text", text: JSON.stringify(result, null, 2) },
234
+ ],
235
+ isError: false,
236
+ });
237
+ }
238
+ case "ping":
239
+ return reply({});
240
+ default:
241
+ return fail(-32601, `Method not found: ${method}`);
242
+ }
243
+ }
244
+ catch (e) {
245
+ return fail(-32603, `Internal error: ${e.message}`);
246
+ }
247
+ }
248
+ // ── stdio transport ────────────────────────────────────────────────────────
249
+ async function runStdio() {
250
+ // Read newline-delimited JSON from stdin, write to stdout. This is the
251
+ // standard MCP stdio transport.
252
+ let buffer = "";
253
+ process.stdin.setEncoding("utf8");
254
+ process.stdin.on("data", async (chunk) => {
255
+ buffer += chunk;
256
+ let nl;
257
+ while ((nl = buffer.indexOf("\n")) >= 0) {
258
+ const line = buffer.slice(0, nl).trim();
259
+ buffer = buffer.slice(nl + 1);
260
+ if (!line)
261
+ continue;
262
+ try {
263
+ const req = JSON.parse(line);
264
+ const res = await handle(req);
265
+ if (res)
266
+ process.stdout.write(JSON.stringify(res) + "\n");
267
+ }
268
+ catch (e) {
269
+ process.stderr.write(`mcp: parse error: ${e.message}\n`);
270
+ }
271
+ }
272
+ });
273
+ return new Promise(resolve => {
274
+ process.stdin.on("end", () => resolve(0));
275
+ process.stdin.on("error", () => resolve(2));
276
+ });
277
+ }
278
+ export async function runMcp(args) {
279
+ SERVER_INFO.version = args.version;
280
+ if (args.mode === "sse") {
281
+ process.stderr.write("great-cto mcp: SSE mode not yet implemented (use --stdio)\n");
282
+ return 2;
283
+ }
284
+ // Notify clients we're ready (some hosts log this)
285
+ process.stderr.write(`great-cto mcp v${args.version} (stdio) — ${TOOLS.length} tools\n`);
286
+ return runStdio();
287
+ }
package/dist/serve.js ADDED
@@ -0,0 +1,158 @@
1
+ // great-cto serve — webhook receiver + outbound notifier (scaffolding).
2
+ //
3
+ // v2.4.0 ships scaffolding + a single working endpoint (POST /webhook/github
4
+ // → run scan, log event). Signature verification, retry/DLQ, and outbound
5
+ // integrations land in v2.5.0.
6
+ //
7
+ // Usage:
8
+ // great-cto serve [--port 3142] [--no-log]
9
+ //
10
+ // Endpoints:
11
+ // POST /webhook/github GitHub event receiver (pull_request.opened →
12
+ // runs scan on PR head, persists summary)
13
+ // POST /webhook/generic Catch-all for ad-hoc integrations. Body persisted
14
+ // to ~/.great_cto/webhook-events.log (JSONL).
15
+ // GET /healthz Liveness probe
16
+ // GET /events Recent event log (last 50)
17
+ //
18
+ // All events are appended to ~/.great_cto/webhook-events.log as JSONL with
19
+ // fields: ts, source, event_type, payload_summary, action_taken.
20
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
21
+ import { createServer } from "node:http";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ const EVENTS_LOG = join(homedir(), ".great_cto", "webhook-events.log");
25
+ function logEvent(ev, noLog) {
26
+ if (noLog)
27
+ return;
28
+ try {
29
+ const dir = join(homedir(), ".great_cto");
30
+ if (!existsSync(dir))
31
+ mkdirSync(dir, { recursive: true });
32
+ appendFileSync(EVENTS_LOG, JSON.stringify(ev) + "\n");
33
+ }
34
+ catch (e) {
35
+ process.stderr.write(`serve: failed to log event: ${e.message}\n`);
36
+ }
37
+ }
38
+ function readBody(req) {
39
+ return new Promise((resolve, reject) => {
40
+ let data = "";
41
+ req.on("data", chunk => (data += chunk));
42
+ req.on("end", () => resolve(data));
43
+ req.on("error", reject);
44
+ });
45
+ }
46
+ function json(res, status, body) {
47
+ res.writeHead(status, { "Content-Type": "application/json" });
48
+ res.end(JSON.stringify(body));
49
+ }
50
+ // ── Endpoint handlers ──────────────────────────────────────────────────────
51
+ async function handleGitHub(req, res, args) {
52
+ const body = await readBody(req);
53
+ const eventType = req.headers["x-github-event"] ?? "unknown";
54
+ const deliveryId = req.headers["x-github-delivery"] ?? "no-id";
55
+ let payload;
56
+ try {
57
+ payload = JSON.parse(body);
58
+ }
59
+ catch {
60
+ json(res, 400, { error: "invalid JSON" });
61
+ return;
62
+ }
63
+ // Currently we just record the event. Scan-on-PR lands in v2.5.0 with
64
+ // proper signature verification and clone-and-scan flow.
65
+ const summary = eventType === "pull_request"
66
+ ? `${payload.action ?? "?"} PR #${payload.number ?? "?"} in ${payload.repository?.full_name ?? "?"}`
67
+ : `${eventType} delivery=${deliveryId}`;
68
+ const ev = {
69
+ ts: new Date().toISOString(),
70
+ source: "github",
71
+ event_type: eventType,
72
+ summary,
73
+ action_taken: "logged",
74
+ meta: { delivery_id: deliveryId, pr_number: payload.number, action: payload.action },
75
+ };
76
+ logEvent(ev, args.noLog);
77
+ json(res, 200, { ok: true, event_type: eventType, recorded: true });
78
+ }
79
+ async function handleGeneric(req, res, args) {
80
+ const body = await readBody(req);
81
+ let payload = body;
82
+ try {
83
+ payload = JSON.parse(body);
84
+ }
85
+ catch {
86
+ /* keep as raw string */
87
+ }
88
+ const ev = {
89
+ ts: new Date().toISOString(),
90
+ source: "generic",
91
+ event_type: "incoming",
92
+ summary: `payload ${body.length} bytes`,
93
+ action_taken: "logged",
94
+ meta: { payload_preview: String(body).slice(0, 200) },
95
+ };
96
+ logEvent(ev, args.noLog);
97
+ json(res, 200, { ok: true, recorded: true });
98
+ }
99
+ function handleEvents(_req, res) {
100
+ if (!existsSync(EVENTS_LOG)) {
101
+ json(res, 200, { events: [] });
102
+ return;
103
+ }
104
+ const lines = readFileSync(EVENTS_LOG, "utf8")
105
+ .split("\n")
106
+ .filter(Boolean)
107
+ .slice(-50);
108
+ const events = lines
109
+ .map(l => {
110
+ try {
111
+ return JSON.parse(l);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ })
117
+ .filter(Boolean);
118
+ json(res, 200, { events });
119
+ }
120
+ // ── Main entry ─────────────────────────────────────────────────────────────
121
+ export async function runServe(args) {
122
+ const server = createServer(async (req, res) => {
123
+ const url = new URL(req.url ?? "/", `http://localhost:${args.port}`);
124
+ const path = url.pathname;
125
+ // Healthz
126
+ if (req.method === "GET" && path === "/healthz") {
127
+ return json(res, 200, { ok: true, service: "great-cto serve", events_log: EVENTS_LOG });
128
+ }
129
+ if (req.method === "GET" && path === "/events") {
130
+ return handleEvents(req, res);
131
+ }
132
+ if (req.method === "POST" && path === "/webhook/github") {
133
+ return handleGitHub(req, res, args);
134
+ }
135
+ if (req.method === "POST" && path === "/webhook/generic") {
136
+ return handleGeneric(req, res, args);
137
+ }
138
+ json(res, 404, { error: "not found", path });
139
+ });
140
+ return new Promise(resolve => {
141
+ server.listen(args.port, "127.0.0.1", () => {
142
+ console.error(`great-cto serve → http://localhost:${args.port}`);
143
+ console.error(` POST /webhook/github GitHub event receiver`);
144
+ console.error(` POST /webhook/generic Catch-all`);
145
+ console.error(` GET /events Recent event log`);
146
+ console.error(` GET /healthz Liveness probe`);
147
+ console.error(` log: ${EVENTS_LOG}`);
148
+ });
149
+ process.on("SIGINT", () => {
150
+ server.close();
151
+ resolve(0);
152
+ });
153
+ process.on("SIGTERM", () => {
154
+ server.close();
155
+ resolve(0);
156
+ });
157
+ });
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.3.3",
3
+ "version": "2.4.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",