great-cto 2.3.4 → 2.5.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 +135 -0
- package/dist/mcp.js +355 -0
- package/dist/report.js +410 -0
- package/dist/serve.js +289 -0
- package/dist/webhook-cli.js +150 -0
- package/dist/webhook-config.js +65 -0
- package/dist/webhook-dispatch.js +132 -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
|
+
}
|