great-cto 2.3.4 → 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 +342 -0
- package/dist/ci.js +258 -0
- package/dist/main.js +99 -0
- package/dist/mcp.js +287 -0
- package/dist/serve.js +158 -0
- package/package.json +1 -1
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, "&")
|
|
68
|
+
.replace(/</g, "<")
|
|
69
|
+
.replace(/>/g, ">")
|
|
70
|
+
.replace(/"/g, """)
|
|
71
|
+
.replace(/'/g, "'");
|
|
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