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 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
+ }